Merge "[Reskin] Update string of incompatible charging on battery settings" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index ce311d0..0916227 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -74,6 +74,7 @@
     ":android.chre.flags-aconfig-java{.generated_srcjars}",
     ":android.speech.flags-aconfig-java{.generated_srcjars}",
     ":power_flags_lib{.generated_srcjars}",
+    ":android.content.flags-aconfig-java{.generated_srcjars}",
 ]
 
 filegroup {
@@ -940,3 +941,16 @@
     aconfig_declarations: "power_flags",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
+
+// Content
+aconfig_declarations {
+    name: "android.content.flags-aconfig",
+    package: "android.content.flags",
+    srcs: ["core/java/android/content/flags/flags.aconfig"],
+}
+
+java_aconfig_library {
+    name: "android.content.flags-aconfig-java",
+    aconfig_declarations: "android.content.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/core/api/current.txt b/core/api/current.txt
index 46c8f82..7d5079a 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -1602,7 +1602,6 @@
     field public static final int switchTextOff = 16843628; // 0x101036c
     field public static final int switchTextOn = 16843627; // 0x101036b
     field public static final int syncable = 16842777; // 0x1010019
-    field @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") public static final int systemUserOnly;
     field public static final int tabStripEnabled = 16843453; // 0x10102bd
     field public static final int tabStripLeft = 16843451; // 0x10102bb
     field public static final int tabStripRight = 16843452; // 0x10102bc
@@ -4625,9 +4624,9 @@
 
   public class ActivityManager {
     method public int addAppTask(@NonNull android.app.Activity, @NonNull android.content.Intent, @Nullable android.app.ActivityManager.TaskDescription, @NonNull android.graphics.Bitmap);
+    method @FlaggedApi("android.app.app_start_info") public void addApplicationStartInfoCompletionListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.ApplicationStartInfo>);
     method @FlaggedApi("android.app.app_start_info") public void addStartInfoTimestamp(@IntRange(from=android.app.ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER_START, to=android.app.ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER) int, long);
     method public void appNotResponding(@NonNull String);
-    method @FlaggedApi("android.app.app_start_info") public void clearApplicationStartInfoCompletionListener();
     method public boolean clearApplicationUserData();
     method public void clearWatchHeapLimit();
     method @RequiresPermission(android.Manifest.permission.DUMP) public void dumpPackageState(java.io.FileDescriptor, String);
@@ -4661,8 +4660,8 @@
     method @RequiresPermission(android.Manifest.permission.KILL_BACKGROUND_PROCESSES) public void killBackgroundProcesses(String);
     method @RequiresPermission(android.Manifest.permission.REORDER_TASKS) public void moveTaskToFront(int, int);
     method @RequiresPermission(android.Manifest.permission.REORDER_TASKS) public void moveTaskToFront(int, int, android.os.Bundle);
+    method @FlaggedApi("android.app.app_start_info") public void removeApplicationStartInfoCompletionListener(@NonNull java.util.function.Consumer<android.app.ApplicationStartInfo>);
     method @Deprecated public void restartPackage(String);
-    method @FlaggedApi("android.app.app_start_info") public void setApplicationStartInfoCompletionListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.ApplicationStartInfo>);
     method public void setProcessStateSummary(@Nullable byte[]);
     method public static void setVrThread(int);
     method public void setWatchHeapLimit(long);
@@ -10387,6 +10386,7 @@
     method @CheckResult(suggest="#enforceCallingPermission(String,String)") public abstract int checkCallingPermission(@NonNull String);
     method @CheckResult(suggest="#enforceCallingUriPermission(Uri,int,String)") public abstract int checkCallingUriPermission(android.net.Uri, int);
     method @NonNull public int[] checkCallingUriPermissions(@NonNull java.util.List<android.net.Uri>, int);
+    method @FlaggedApi("android.security.content_uri_permission_apis") public int checkContentUriPermissionFull(@NonNull android.net.Uri, int, int, int);
     method @CheckResult(suggest="#enforcePermission(String,int,int,String)") public abstract int checkPermission(@NonNull String, int, int);
     method public abstract int checkSelfPermission(@NonNull String);
     method @CheckResult(suggest="#enforceUriPermission(Uri,int,int,String)") public abstract int checkUriPermission(android.net.Uri, int, int, int);
@@ -10545,6 +10545,7 @@
     field public static final int BIND_INCLUDE_CAPABILITIES = 4096; // 0x1000
     field public static final int BIND_NOT_FOREGROUND = 4; // 0x4
     field public static final int BIND_NOT_PERCEPTIBLE = 256; // 0x100
+    field @FlaggedApi("android.content.flags.enable_bind_package_isolated_process") public static final int BIND_PACKAGE_ISOLATED_PROCESS = 16384; // 0x4000
     field public static final int BIND_SHARED_ISOLATED_PROCESS = 8192; // 0x2000
     field public static final int BIND_WAIVE_PRIORITY = 32; // 0x20
     field public static final String BIOMETRIC_SERVICE = "biometric";
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index c7e314c..d26662d 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3221,6 +3221,7 @@
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
     method @NonNull public android.content.Context createContext();
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.audio.VirtualAudioDevice createVirtualAudioDevice(@NonNull android.hardware.display.VirtualDisplay, @Nullable java.util.concurrent.Executor, @Nullable android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback);
+    method @FlaggedApi("android.companion.virtual.flags.virtual_camera") @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.camera.VirtualCamera createVirtualCamera(@NonNull android.companion.virtual.camera.VirtualCameraConfig);
     method @Deprecated @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, @Nullable android.view.Surface, int, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
     method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull android.hardware.display.VirtualDisplayConfig, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
     method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualDpad createVirtualDpad(@NonNull android.hardware.input.VirtualDpadConfig);
@@ -3275,6 +3276,7 @@
     field @Deprecated public static final int NAVIGATION_POLICY_DEFAULT_BLOCKED = 1; // 0x1
     field @FlaggedApi("android.companion.virtual.flags.dynamic_policy") public static final int POLICY_TYPE_ACTIVITY = 3; // 0x3
     field public static final int POLICY_TYPE_AUDIO = 1; // 0x1
+    field @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final int POLICY_TYPE_CAMERA = 5; // 0x5
     field @FlaggedApi("android.companion.virtual.flags.cross_device_clipboard") public static final int POLICY_TYPE_CLIPBOARD = 4; // 0x4
     field public static final int POLICY_TYPE_RECENTS = 2; // 0x2
     field public static final int POLICY_TYPE_SENSORS = 0; // 0x0
@@ -3357,30 +3359,38 @@
   @FlaggedApi("android.companion.virtual.flags.virtual_camera") public interface VirtualCameraCallback {
     method public default void onProcessCaptureRequest(int, long);
     method public void onStreamClosed(int);
-    method public void onStreamConfigured(int, @NonNull android.view.Surface, @NonNull android.companion.virtual.camera.VirtualCameraStreamConfig);
+    method public void onStreamConfigured(int, @NonNull android.view.Surface, @IntRange(from=1) int, @IntRange(from=1) int, int);
   }
 
   @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraConfig implements android.os.Parcelable {
     method public int describeContents();
+    method public int getLensFacing();
     method @NonNull public String getName();
+    method public int getSensorOrientation();
     method @NonNull public java.util.Set<android.companion.virtual.camera.VirtualCameraStreamConfig> getStreamConfigs();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraConfig> CREATOR;
+    field public static final int SENSOR_ORIENTATION_0 = 0; // 0x0
+    field public static final int SENSOR_ORIENTATION_180 = 180; // 0xb4
+    field public static final int SENSOR_ORIENTATION_270 = 270; // 0x10e
+    field public static final int SENSOR_ORIENTATION_90 = 90; // 0x5a
   }
 
   @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final class VirtualCameraConfig.Builder {
     ctor public VirtualCameraConfig.Builder();
-    method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder addStreamConfig(int, int, int);
+    method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder addStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int, @IntRange(from=1) int);
     method @NonNull public android.companion.virtual.camera.VirtualCameraConfig build();
+    method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setLensFacing(int);
     method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setName(@NonNull String);
+    method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setSensorOrientation(int);
     method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setVirtualCameraCallback(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.camera.VirtualCameraCallback);
   }
 
   @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraStreamConfig implements android.os.Parcelable {
-    ctor public VirtualCameraStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int);
     method public int describeContents();
     method public int getFormat();
     method @IntRange(from=1) public int getHeight();
+    method @IntRange(from=1) public int getMaximumFramesPerSecond();
     method @IntRange(from=1) public int getWidth();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraStreamConfig> CREATOR;
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 6b7f4880..ebb5ba0 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -4052,10 +4052,28 @@
         }
     }
 
+    private final ArrayList<AppStartInfoCallbackWrapper> mAppStartInfoCallbacks =
+            new ArrayList<>();
+    @Nullable
+    private IApplicationStartInfoCompleteListener mAppStartInfoCompleteListener = null;
+
+    private static final class AppStartInfoCallbackWrapper {
+        @NonNull final Executor mExecutor;
+        @NonNull final Consumer<ApplicationStartInfo> mListener;
+
+        AppStartInfoCallbackWrapper(@NonNull final Executor executor,
+                @NonNull final Consumer<ApplicationStartInfo> listener) {
+            mExecutor = executor;
+            mListener = listener;
+        }
+    }
+
     /**
-     * Sets a callback to be notified when the {@link ApplicationStartInfo} records of this startup
+     * Adds a callback to be notified when the {@link ApplicationStartInfo} records of this startup
      * are complete.
      *
+     * <p class="note"> Note: callback will be removed automatically after being triggered.</p>
+     *
      * <p class="note"> Note: callback will not wait for {@link Activity#reportFullyDrawn} to occur.
      * Timestamp for fully drawn may be added after callback occurs. Set callback after invoking
      * {@link Activity#reportFullyDrawn} if timestamp for fully drawn is required.</p>
@@ -4073,33 +4091,77 @@
      * @throws IllegalArgumentException if executor or listener are null.
      */
     @FlaggedApi(Flags.FLAG_APP_START_INFO)
-    public void setApplicationStartInfoCompletionListener(@NonNull final Executor executor,
+    public void addApplicationStartInfoCompletionListener(@NonNull final Executor executor,
             @NonNull final Consumer<ApplicationStartInfo> listener) {
         Preconditions.checkNotNull(executor, "executor cannot be null");
         Preconditions.checkNotNull(listener, "listener cannot be null");
-        IApplicationStartInfoCompleteListener callback =
-                new IApplicationStartInfoCompleteListener.Stub() {
-            @Override
-            public void onApplicationStartInfoComplete(ApplicationStartInfo applicationStartInfo) {
-                executor.execute(() -> listener.accept(applicationStartInfo));
+        synchronized (mAppStartInfoCallbacks) {
+            for (int i = 0; i < mAppStartInfoCallbacks.size(); i++) {
+                if (listener.equals(mAppStartInfoCallbacks.get(i).mListener)) {
+                    return;
+                }
             }
-        };
-        try {
-            getService().setApplicationStartInfoCompleteListener(callback, mContext.getUserId());
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            if (mAppStartInfoCompleteListener == null) {
+                mAppStartInfoCompleteListener = new IApplicationStartInfoCompleteListener.Stub() {
+                    @Override
+                    public void onApplicationStartInfoComplete(
+                            ApplicationStartInfo applicationStartInfo) {
+                        synchronized (mAppStartInfoCallbacks) {
+                            for (int i = 0; i < mAppStartInfoCallbacks.size(); i++) {
+                                final AppStartInfoCallbackWrapper callback =
+                                        mAppStartInfoCallbacks.get(i);
+                                callback.mExecutor.execute(() -> callback.mListener.accept(
+                                        applicationStartInfo));
+                            }
+                            mAppStartInfoCallbacks.clear();
+                            mAppStartInfoCompleteListener = null;
+                        }
+                    }
+                };
+                boolean succeeded = false;
+                try {
+                    getService().addApplicationStartInfoCompleteListener(
+                            mAppStartInfoCompleteListener, mContext.getUserId());
+                    succeeded = true;
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                }
+                if (succeeded) {
+                    mAppStartInfoCallbacks.add(new AppStartInfoCallbackWrapper(executor, listener));
+                } else {
+                    mAppStartInfoCompleteListener = null;
+                    mAppStartInfoCallbacks.clear();
+                }
+            } else {
+                mAppStartInfoCallbacks.add(new AppStartInfoCallbackWrapper(executor, listener));
+            }
         }
     }
 
     /**
-     * Removes the callback set by {@link #setApplicationStartInfoCompletionListener} if there is one.
+     * Removes the provided callback set by {@link #addApplicationStartInfoCompletionListener}.
      */
     @FlaggedApi(Flags.FLAG_APP_START_INFO)
-    public void clearApplicationStartInfoCompletionListener() {
-        try {
-            getService().clearApplicationStartInfoCompleteListener(mContext.getUserId());
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+    public void removeApplicationStartInfoCompletionListener(
+            @NonNull final Consumer<ApplicationStartInfo> listener) {
+        Preconditions.checkNotNull(listener, "listener cannot be null");
+        synchronized (mAppStartInfoCallbacks) {
+            for (int i = 0; i < mAppStartInfoCallbacks.size(); i++) {
+                final AppStartInfoCallbackWrapper callback = mAppStartInfoCallbacks.get(i);
+                if (listener.equals(callback.mListener)) {
+                    mAppStartInfoCallbacks.remove(i);
+                    break;
+                }
+            }
+            if (mAppStartInfoCompleteListener != null && mAppStartInfoCallbacks.isEmpty()) {
+                try {
+                    getService().removeApplicationStartInfoCompleteListener(
+                            mAppStartInfoCompleteListener, mContext.getUserId());
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                }
+                mAppStartInfoCompleteListener = null;
+            }
         }
     }
 
diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index 656feb0..cc3eac1 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -39,12 +39,17 @@
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.Iterator;
 import java.util.Map;
-import java.util.Set;
 
 /**
- * Provide information related to a processes startup.
+ * Describes information related to an application process's startup.
+ *
+ * <p>
+ * Many aspects concerning why and how an applications process was started are valuable for apps
+ * both for logging and for potential behavior changes. Reason for process start, start type,
+ * start times, throttling, and other useful diagnostic data can be obtained from
+ * {@link ApplicationStartInfo} records.
+ * </p>
  */
 @FlaggedApi(Flags.FLAG_APP_START_INFO)
 public final class ApplicationStartInfo implements Parcelable {
@@ -552,13 +557,12 @@
         dest.writeInt(mDefiningUid);
         dest.writeString(mProcessName);
         dest.writeInt(mReason);
-        dest.writeInt(mStartupTimestampsNs.size());
-        Set<Map.Entry<Integer, Long>> timestampEntrySet = mStartupTimestampsNs.entrySet();
-        Iterator<Map.Entry<Integer, Long>> iter = timestampEntrySet.iterator();
-        while (iter.hasNext()) {
-            Map.Entry<Integer, Long> entry = iter.next();
-            dest.writeInt(entry.getKey());
-            dest.writeLong(entry.getValue());
+        dest.writeInt(mStartupTimestampsNs == null ? 0 : mStartupTimestampsNs.size());
+        if (mStartupTimestampsNs != null) {
+            for (int i = 0; i < mStartupTimestampsNs.size(); i++) {
+                dest.writeInt(mStartupTimestampsNs.keyAt(i));
+                dest.writeLong(mStartupTimestampsNs.valueAt(i));
+            }
         }
         dest.writeInt(mStartType);
         dest.writeParcelable(mStartIntent, flags);
@@ -740,13 +744,11 @@
             sb.append(" intent=").append(mStartIntent.toString())
                 .append('\n');
         }
-        if (mStartupTimestampsNs.size() > 0) {
+        if (mStartupTimestampsNs != null && mStartupTimestampsNs.size() > 0) {
             sb.append(" timestamps: ");
-            Set<Map.Entry<Integer, Long>> timestampEntrySet = mStartupTimestampsNs.entrySet();
-            Iterator<Map.Entry<Integer, Long>> iter = timestampEntrySet.iterator();
-            while (iter.hasNext()) {
-                Map.Entry<Integer, Long> entry = iter.next();
-                sb.append(entry.getKey()).append("=").append(entry.getValue()).append(" ");
+            for (int i = 0; i < mStartupTimestampsNs.size(); i++) {
+                sb.append(mStartupTimestampsNs.keyAt(i)).append("=").append(mStartupTimestampsNs
+                        .valueAt(i)).append(" ");
             }
             sb.append('\n');
         }
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index edeec77..ed00d9c 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -2409,6 +2409,17 @@
         }
     }
 
+    @Override
+    public int checkContentUriPermissionFull(Uri uri, int pid, int uid, int modeFlags) {
+        try {
+            return ActivityManager.getService().checkContentUriPermissionFull(
+                    ContentProvider.getUriWithoutUserId(uri), pid, uid, modeFlags,
+                    resolveUserId(uri));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     @NonNull
     @Override
     public int[] checkUriPermissions(@NonNull List<Uri> uris, int pid, int uid,
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 260e985..b5d88e8 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -274,6 +274,7 @@
     int getProcessLimit();
     int checkUriPermission(in Uri uri, int pid, int uid, int mode, int userId,
             in IBinder callerToken);
+    int checkContentUriPermissionFull(in Uri uri, int pid, int uid, int mode, int userId);
     int[] checkUriPermissions(in List<Uri> uris, int pid, int uid, int mode, int userId,
                 in IBinder callerToken);
     void grantUriPermission(in IApplicationThread caller, in String targetPkg, in Uri uri,
@@ -715,7 +716,7 @@
      * @param listener    A listener to for the callback upon completion of startup data collection.
      * @param userId      The userId in the multi-user environment.
      */
-    void setApplicationStartInfoCompleteListener(IApplicationStartInfoCompleteListener listener,
+    void addApplicationStartInfoCompleteListener(IApplicationStartInfoCompleteListener listener,
             int userId);
 
 
@@ -724,7 +725,8 @@
      *
      * @param userId      The userId in the multi-user environment.
      */
-    void clearApplicationStartInfoCompleteListener(int userId);
+    void removeApplicationStartInfoCompleteListener(IApplicationStartInfoCompleteListener listener,
+            int userId);
 
 
     /**
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 10b652a..a4cada2 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -901,11 +901,14 @@
         }
 
         /**
-         * Creates a new virtual camera. If a virtual camera was already created, it will be closed.
+         * Creates a new virtual camera with the given {@link VirtualCameraConfig}. A virtual device
+         * can create a virtual camera only if it has
+         * {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM} as its
+         * {@link VirtualDeviceParams#POLICY_TYPE_CAMERA}.
          *
-         * @param config camera config.
-         * @return newly created camera;
-         * @hide
+         * @param config camera configuration.
+         * @return newly created camera.
+         * @see VirtualDeviceParams#POLICY_TYPE_CAMERA
          */
         @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         @NonNull
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index 0253ddd..f9a9da1 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -159,7 +159,7 @@
      * @hide
      */
     @IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_SENSORS, POLICY_TYPE_AUDIO,
-            POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY})
+            POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CAMERA})
     @Retention(RetentionPolicy.SOURCE)
     @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
     public @interface PolicyType {}
@@ -246,6 +246,23 @@
     @FlaggedApi(Flags.FLAG_CROSS_DEVICE_CLIPBOARD)
     public static final int POLICY_TYPE_CLIPBOARD = 4;
 
+    /**
+     * Tells the camera framework how to handle camera requests for the front and back cameras from
+     * contexts associated with this virtual device.
+     *
+     * <ul>
+     *     <li>{@link #DEVICE_POLICY_DEFAULT}: Returns the front and back cameras of the default
+     *     device.
+     *     <li>{@link #DEVICE_POLICY_CUSTOM}: Returns the front and back cameras cameras of the
+     *     virtual device. Note that if the virtual device did not create any virtual cameras,
+     *     then no front and back cameras will be available.
+     * </ul>
+     *
+     * @see Context#getDeviceId
+     */
+    @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
+    public static final int POLICY_TYPE_CAMERA = 5;
+
     private final int mLockState;
     @NonNull private final ArraySet<UserHandle> mUsersWithMatchingAccounts;
     @NavigationPolicy
@@ -1153,6 +1170,10 @@
                 mDevicePolicies.delete(POLICY_TYPE_CLIPBOARD);
             }
 
+            if (!Flags.virtualCamera()) {
+                mDevicePolicies.delete(POLICY_TYPE_CAMERA);
+            }
+
             if ((mAudioPlaybackSessionId != AUDIO_SESSION_ID_GENERATE
                     || mAudioRecordingSessionId != AUDIO_SESSION_ID_GENERATE)
                     && mDevicePolicies.get(POLICY_TYPE_AUDIO, DEVICE_POLICY_DEFAULT)
diff --git a/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
index 44942d6..f4f69b5 100644
--- a/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
+++ b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package android.companion.virtual.camera;
 
-import android.companion.virtual.camera.VirtualCameraStreamConfig;
 import android.view.Surface;
 
 /**
@@ -30,38 +30,36 @@
      * Called when one of the requested stream has been configured by the virtual camera service and
      * is ready to receive data onto its {@link Surface}
      *
-     * @param streamId     The id of the configured stream
-     * @param surface      The surface to write data into for this stream
-     * @param streamConfig The image data configuration for this stream
+     * @param streamId The id of the configured stream
+     * @param surface The surface to write data into for this stream
+     * @param width The width of the surface
+     * @param height The height of the surface
+     * @param format The pixel format of the surface
      */
-    oneway void onStreamConfigured(
-            int streamId,
-            in Surface surface,
-            in VirtualCameraStreamConfig streamConfig);
+    oneway void onStreamConfigured(int streamId, in Surface surface, int width, int height,
+            int format);
 
     /**
      * The client application is requesting a camera frame for the given streamId and frameId.
      *
      * <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to
-     * this stream that was provided during the {@link #onStreamConfigured(int, Surface,
-     * VirtualCameraStreamConfig)} call.
+     * this stream that was provided during the
+     * {@link #onStreamConfigured(int, Surface, int, int, int)} call.
      *
      * @param streamId The streamId for which the frame is requested. This corresponds to the
-     *     streamId that was given in {@link #onStreamConfigured(int, Surface,
-     *     VirtualCameraStreamConfig)}
+     *     streamId that was given in {@link #onStreamConfigured(int, Surface, int, int, int)}
      * @param frameId The frameId that is being requested. Each request will have a different
      *     frameId, that will be increasing for each call with a particular streamId.
      */
     oneway void onProcessCaptureRequest(int streamId, long frameId);
 
     /**
-     * The stream previously configured when {@link #onStreamConfigured(int, Surface,
-     * VirtualCameraStreamConfig)} was called is now being closed and associated resources can be
-     * freed. The Surface was disposed on the client side and should not be used anymore by the
-     * virtual camera owner.
+     * The stream previously configured when
+     * {@link #onStreamConfigured(int, Surface, int, int, int)} was called is now being closed and
+     * associated resources can be freed. The Surface was disposed on the client side and should not
+     * be used anymore by the virtual camera owner.
      *
      * @param streamId The id of the stream that was closed.
      */
     oneway void onStreamClosed(int streamId);
-
 }
\ No newline at end of file
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
index 5b658b8..c894de4 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
@@ -17,9 +17,11 @@
 package android.companion.virtual.camera;
 
 import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.companion.virtual.flags.Flags;
+import android.graphics.ImageFormat;
 import android.view.Surface;
 
 import java.util.concurrent.Executor;
@@ -41,33 +43,33 @@
      *
      * @param streamId The id of the configured stream
      * @param surface The surface to write data into for this stream
-     * @param streamConfig The image data configuration for this stream
+     * @param width The width of the surface
+     * @param height The height of the surface
+     * @param format The {@link ImageFormat} of the surface
      */
-    void onStreamConfigured(
-            int streamId,
-            @NonNull Surface surface,
-            @NonNull VirtualCameraStreamConfig streamConfig);
+    void onStreamConfigured(int streamId, @NonNull Surface surface,
+            @IntRange(from = 1) int width, @IntRange(from = 1) int height,
+            @ImageFormat.Format int format);
 
     /**
      * The client application is requesting a camera frame for the given streamId and frameId.
      *
      * <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to
-     * this stream that was provided during the {@link #onStreamConfigured(int, Surface,
-     * VirtualCameraStreamConfig)} call.
+     * this stream that was provided during the
+     * {@link #onStreamConfigured(int, Surface, int, int, int)} call.
      *
      * @param streamId The streamId for which the frame is requested. This corresponds to the
-     *     streamId that was given in {@link #onStreamConfigured(int, Surface,
-     *     VirtualCameraStreamConfig)}
+     *     streamId that was given in {@link #onStreamConfigured(int, Surface, int, int, int)}
      * @param frameId The frameId that is being requested. Each request will have a different
      *     frameId, that will be increasing for each call with a particular streamId.
      */
     default void onProcessCaptureRequest(int streamId, long frameId) {}
 
     /**
-     * The stream previously configured when {@link #onStreamConfigured(int, Surface,
-     * VirtualCameraStreamConfig)} was called is now being closed and associated resources can be
-     * freed. The Surface corresponding to that streamId was disposed on the client side and should
-     * not be used anymore by the virtual camera owner
+     * The stream previously configured when
+     * {@link #onStreamConfigured(int, Surface, int, int, int)} was called is now being closed and
+     * associated resources can be freed. The Surface corresponding to that streamId was disposed on
+     * the client side and should not be used anymore by the virtual camera owner.
      *
      * @param streamId The id of the stream that was closed.
      */
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl b/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl
index 88c27a5..5ceb4d1 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package android.companion.virtual.camera;
 
 /** @hide */
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
index 59fe9a1..350cf3d 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
@@ -19,16 +19,23 @@
 import static java.util.Objects.requireNonNull;
 
 import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
+import android.companion.virtual.VirtualDevice;
 import android.companion.virtual.flags.Flags;
 import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
+import android.hardware.camera2.CameraMetadata;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.ArraySet;
 import android.view.Surface;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Set;
 import java.util.concurrent.Executor;
 
@@ -43,16 +50,57 @@
 @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
 public final class VirtualCameraConfig implements Parcelable {
 
+    private static final int LENS_FACING_UNKNOWN = -1;
+
+    /**
+     * Sensor orientation of {@code 0} degrees.
+     * @see #getSensorOrientation
+     */
+    public static final int SENSOR_ORIENTATION_0 = 0;
+    /**
+     * Sensor orientation of {@code 90} degrees.
+     * @see #getSensorOrientation
+     */
+    public static final int SENSOR_ORIENTATION_90 = 90;
+    /**
+     * Sensor orientation of {@code 180} degrees.
+     * @see #getSensorOrientation
+     */
+    public static final int SENSOR_ORIENTATION_180 = 180;
+    /**
+     * Sensor orientation of {@code 270} degrees.
+     * @see #getSensorOrientation
+     */
+    public static final int SENSOR_ORIENTATION_270 = 270;
+    /** @hide */
+    @IntDef(prefix = {"SENSOR_ORIENTATION_"}, value = {
+            SENSOR_ORIENTATION_0,
+            SENSOR_ORIENTATION_90,
+            SENSOR_ORIENTATION_180,
+            SENSOR_ORIENTATION_270
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SensorOrientation {}
+
     private final String mName;
     private final Set<VirtualCameraStreamConfig> mStreamConfigurations;
     private final IVirtualCameraCallback mCallback;
+    @SensorOrientation
+    private final int mSensorOrientation;
+    private final int mLensFacing;
 
     private VirtualCameraConfig(
             @NonNull String name,
             @NonNull Set<VirtualCameraStreamConfig> streamConfigurations,
             @NonNull Executor executor,
-            @NonNull VirtualCameraCallback callback) {
+            @NonNull VirtualCameraCallback callback,
+            @SensorOrientation int sensorOrientation,
+            int lensFacing) {
         mName = requireNonNull(name, "Missing name");
+        if (lensFacing == LENS_FACING_UNKNOWN) {
+            throw new IllegalArgumentException("Lens facing must be set");
+        }
+        mLensFacing = lensFacing;
         mStreamConfigurations =
                 Set.copyOf(requireNonNull(streamConfigurations, "Missing stream configurations"));
         if (mStreamConfigurations.isEmpty()) {
@@ -63,6 +111,7 @@
                 new VirtualCameraCallbackInternal(
                         requireNonNull(callback, "Missing callback"),
                         requireNonNull(executor, "Missing callback executor"));
+        mSensorOrientation = sensorOrientation;
     }
 
     private VirtualCameraConfig(@NonNull Parcel in) {
@@ -73,6 +122,8 @@
                         in.readParcelableArray(
                                 VirtualCameraStreamConfig.class.getClassLoader(),
                                 VirtualCameraStreamConfig.class));
+        mSensorOrientation = in.readInt();
+        mLensFacing = in.readInt();
     }
 
     @Override
@@ -86,6 +137,8 @@
         dest.writeStrongInterface(mCallback);
         dest.writeParcelableArray(
                 mStreamConfigurations.toArray(new VirtualCameraStreamConfig[0]), flags);
+        dest.writeInt(mSensorOrientation);
+        dest.writeInt(mLensFacing);
     }
 
     /**
@@ -100,7 +153,7 @@
      * Returns an unmodifiable set of the stream configurations added to this {@link
      * VirtualCameraConfig}.
      *
-     * @see VirtualCameraConfig.Builder#addStreamConfig(int, int, int)
+     * @see VirtualCameraConfig.Builder#addStreamConfig(int, int, int, int)
      */
     @NonNull
     public Set<VirtualCameraStreamConfig> getStreamConfigs() {
@@ -118,13 +171,33 @@
     }
 
     /**
+     * Returns the sensor orientation of this stream, which represents the clockwise angle (in
+     * degrees) through which the output image needs to be rotated to be upright on the device
+     * screen in its native orientation. Returns {@link #SENSOR_ORIENTATION_0} if omitted.
+     */
+    @SensorOrientation
+    public int getSensorOrientation() {
+        return mSensorOrientation;
+    }
+
+    /**
+     * Returns the direction that the virtual camera faces relative to the virtual device's screen.
+     *
+     * @see Builder#setLensFacing(int)
+     */
+    public int getLensFacing() {
+        return mLensFacing;
+    }
+
+    /**
      * Builder for {@link VirtualCameraConfig}.
      *
      * <p>To build an instance of {@link VirtualCameraConfig} the following conditions must be met:
-     * <li>At least one stream must be added with {@link #addStreamConfig(int, int, int)}.
+     * <li>At least one stream must be added with {@link #addStreamConfig(int, int, int, int)}.
      * <li>A callback must be set with {@link #setVirtualCameraCallback(Executor,
      *     VirtualCameraCallback)}
      * <li>A camera name must be set with {@link #setName(String)}
+     * <li>A lens facing must be set with {@link #setLensFacing(int)}
      */
     @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
     public static final class Builder {
@@ -133,9 +206,11 @@
         private final ArraySet<VirtualCameraStreamConfig> mStreamConfigurations = new ArraySet<>();
         private Executor mCallbackExecutor;
         private VirtualCameraCallback mCallback;
+        private int mSensorOrientation = SENSOR_ORIENTATION_0;
+        private int mLensFacing = LENS_FACING_UNKNOWN;
 
         /**
-         * Set the name of the virtual camera instance.
+         * Sets the name of the virtual camera instance.
          */
         @NonNull
         public Builder setName(@NonNull String name) {
@@ -144,25 +219,94 @@
         }
 
         /**
-         * Add an available stream configuration fot this {@link VirtualCamera}.
+         * Adds a supported input stream configuration for this {@link VirtualCamera}.
          *
          * <p>At least one {@link VirtualCameraStreamConfig} must be added.
          *
          * @param width The width of the stream.
          * @param height The height of the stream.
-         * @param format The {@link ImageFormat} of the stream.
+         * @param format The input format of the stream. Supported formats are
+         *               {@link ImageFormat#YUV_420_888} and {@link PixelFormat#RGBA_8888}.
+         * @param maximumFramesPerSecond The maximum frame rate (in frames per second) for the
+         *                               stream.
          *
-         * @throws IllegalArgumentException if invalid format or dimensions are passed.
+         * @throws IllegalArgumentException if invalid dimensions, format or frame rate are passed.
          */
         @NonNull
-        public Builder addStreamConfig(int width, int height, @ImageFormat.Format int format) {
-            if (width <= 0 || height <= 0) {
-                throw new IllegalArgumentException("Invalid dimensions passed for stream config");
+        public Builder addStreamConfig(
+                @IntRange(from = 1) int width,
+                @IntRange(from = 1) int height,
+                @ImageFormat.Format int format,
+                @IntRange(from = 1) int maximumFramesPerSecond) {
+            // TODO(b/310857519): Check dimension upper limits based on the maximum texture size
+            // supported by the current device, instead of hardcoded limits.
+            if (width <= 0 || width > VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT) {
+                throw new IllegalArgumentException(
+                        "Invalid width passed for stream config: " + width
+                                + ", must be between 1 and "
+                                + VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT);
             }
-            if (!ImageFormat.isPublicFormat(format)) {
-                throw new IllegalArgumentException("Invalid format passed for stream config");
+            if (height <= 0 || height > VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT) {
+                throw new IllegalArgumentException(
+                        "Invalid height passed for stream config: " + height
+                                + ", must be between 1 and "
+                                + VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT);
             }
-            mStreamConfigurations.add(new VirtualCameraStreamConfig(width, height, format));
+            if (!isFormatSupported(format)) {
+                throw new IllegalArgumentException(
+                        "Invalid format passed for stream config: " + format);
+            }
+            if (maximumFramesPerSecond <= 0
+                    || maximumFramesPerSecond > VirtualCameraStreamConfig.MAX_FPS_UPPER_LIMIT) {
+                throw new IllegalArgumentException(
+                        "Invalid maximumFramesPerSecond, must be greater than 0 and less than "
+                                + VirtualCameraStreamConfig.MAX_FPS_UPPER_LIMIT);
+            }
+            mStreamConfigurations.add(new VirtualCameraStreamConfig(width, height, format,
+                    maximumFramesPerSecond));
+            return this;
+        }
+
+        /**
+         * Sets the sensor orientation of the virtual camera. This field is optional and can be
+         * omitted (defaults to {@link #SENSOR_ORIENTATION_0}).
+         *
+         * @param sensorOrientation The sensor orientation of the camera, which represents the
+         *                          clockwise angle (in degrees) through which the output image
+         *                          needs to be rotated to be upright on the device screen in its
+         *                          native orientation.
+         */
+        @NonNull
+        public Builder setSensorOrientation(@SensorOrientation int sensorOrientation) {
+            if (sensorOrientation != SENSOR_ORIENTATION_0
+                    && sensorOrientation != SENSOR_ORIENTATION_90
+                    && sensorOrientation != SENSOR_ORIENTATION_180
+                    && sensorOrientation != SENSOR_ORIENTATION_270) {
+                throw new IllegalArgumentException(
+                        "Invalid sensor orientation: " + sensorOrientation);
+            }
+            mSensorOrientation = sensorOrientation;
+            return this;
+        }
+
+        /**
+         * Sets the lens facing direction of the virtual camera, can be either
+         * {@link CameraMetadata#LENS_FACING_FRONT} or {@link CameraMetadata#LENS_FACING_BACK}.
+         *
+         * <p>A {@link VirtualDevice} can have at most one {@link VirtualCamera} with
+         * {@link CameraMetadata#LENS_FACING_FRONT} and at most one {@link VirtualCamera} with
+         * {@link CameraMetadata#LENS_FACING_BACK}.
+         *
+         * @param lensFacing The direction that the virtual camera faces relative to the device's
+         *                   screen.
+         */
+        @NonNull
+        public Builder setLensFacing(int lensFacing) {
+            if (lensFacing != CameraMetadata.LENS_FACING_BACK
+                    && lensFacing != CameraMetadata.LENS_FACING_FRONT) {
+                throw new IllegalArgumentException("Unsupported lens facing: " + lensFacing);
+            }
+            mLensFacing = lensFacing;
             return this;
         }
 
@@ -189,11 +333,13 @@
          * Builds a new instance of {@link VirtualCameraConfig}
          *
          * @throws NullPointerException if some required parameters are missing.
+         * @throws IllegalArgumentException if any parameter is invalid.
          */
         @NonNull
         public VirtualCameraConfig build() {
             return new VirtualCameraConfig(
-                    mName, mStreamConfigurations, mCallbackExecutor, mCallback);
+                    mName, mStreamConfigurations, mCallbackExecutor, mCallback, mSensorOrientation,
+                    mLensFacing);
         }
     }
 
@@ -208,9 +354,10 @@
         }
 
         @Override
-        public void onStreamConfigured(
-                int streamId, Surface surface, VirtualCameraStreamConfig streamConfig) {
-            mExecutor.execute(() -> mCallback.onStreamConfigured(streamId, surface, streamConfig));
+        public void onStreamConfigured(int streamId, Surface surface, int width, int height,
+                int format) {
+            mExecutor.execute(() -> mCallback.onStreamConfigured(streamId, surface, width, height,
+                    format));
         }
 
         @Override
@@ -237,4 +384,11 @@
                     return new VirtualCameraConfig[size];
                 }
             };
+
+    private static boolean isFormatSupported(@ImageFormat.Format int format) {
+        return switch (format) {
+            case ImageFormat.YUV_420_888, PixelFormat.RGBA_8888 -> true;
+            default -> false;
+        };
+    }
 }
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
deleted file mode 100644
index ce92b6d..0000000
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.companion.virtual.camera;
-
-/**
- * The configuration of a single virtual camera stream.
- * @hide
- */
-parcelable VirtualCameraStreamConfig;
\ No newline at end of file
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
index 1272f16..00a814e 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package android.companion.virtual.camera;
 
 import android.annotation.FlaggedApi;
@@ -24,6 +25,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.util.Objects;
 
 /**
@@ -34,32 +37,47 @@
 @SystemApi
 @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
 public final class VirtualCameraStreamConfig implements Parcelable {
+    // TODO(b/310857519): Check if we should increase the fps upper limit in future.
+    static final int MAX_FPS_UPPER_LIMIT = 60;
+    // This is the minimum guaranteed upper bound of texture size supported by all devices.
+    // Keep this in sync with kMaxTextureSize from services/camera/virtualcamera/util/Util.cc
+    // TODO(b/310857519): Remove this once we add support for fetching the maximum texture size
+    // supported by the current device.
+    static final int DIMENSION_UPPER_LIMIT = 2048;
 
     private final int mWidth;
     private final int mHeight;
     private final int mFormat;
+    private final int mMaxFps;
 
     /**
      * Construct a new instance of {@link VirtualCameraStreamConfig} initialized with the provided
-     * width, height and {@link ImageFormat}
+     * width, height and {@link ImageFormat}.
      *
      * @param width The width of the stream.
      * @param height The height of the stream.
      * @param format The {@link ImageFormat} of the stream.
+     * @param maxFps The maximum frame rate (in frames per second) for the stream.
+     *
+     * @hide
      */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public VirtualCameraStreamConfig(
             @IntRange(from = 1) int width,
             @IntRange(from = 1) int height,
-            @ImageFormat.Format int format) {
+            @ImageFormat.Format int format,
+            @IntRange(from = 1) int maxFps) {
         this.mWidth = width;
         this.mHeight = height;
         this.mFormat = format;
+        this.mMaxFps = maxFps;
     }
 
     private VirtualCameraStreamConfig(@NonNull Parcel in) {
         mWidth = in.readInt();
         mHeight = in.readInt();
         mFormat = in.readInt();
+        mMaxFps = in.readInt();
     }
 
     @Override
@@ -72,6 +90,7 @@
         dest.writeInt(mWidth);
         dest.writeInt(mHeight);
         dest.writeInt(mFormat);
+        dest.writeInt(mMaxFps);
     }
 
     @NonNull
@@ -105,12 +124,13 @@
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
         VirtualCameraStreamConfig that = (VirtualCameraStreamConfig) o;
-        return mWidth == that.mWidth && mHeight == that.mHeight && mFormat == that.mFormat;
+        return mWidth == that.mWidth && mHeight == that.mHeight && mFormat == that.mFormat
+                && mMaxFps == that.mMaxFps;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mWidth, mHeight, mFormat);
+        return Objects.hash(mWidth, mHeight, mFormat, mMaxFps);
     }
 
     /** Returns the {@link ImageFormat} of this stream. */
@@ -118,4 +138,10 @@
     public int getFormat() {
         return mFormat;
     }
+
+    /** Returns the maximum frame rate (in frames per second) of this stream. */
+    @IntRange(from = 1)
+    public int getMaximumFramesPerSecond() {
+        return mMaxFps;
+    }
 }
diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java
index e9b94c9..c7a75ed 100644
--- a/core/java/android/content/ContentProvider.java
+++ b/core/java/android/content/ContentProvider.java
@@ -41,7 +41,6 @@
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.database.SQLException;
-import android.multiuser.Flags;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Binder;
@@ -147,7 +146,6 @@
     private boolean mExported;
     private boolean mNoPerms;
     private boolean mSingleUser;
-    private boolean mSystemUserOnly;
     private SparseBooleanArray mUsersRedirectedToOwnerForMedia = new SparseBooleanArray();
 
     private ThreadLocal<AttributionSource> mCallingAttributionSource;
@@ -379,9 +377,7 @@
                             != PermissionChecker.PERMISSION_GRANTED
                             && getContext().checkUriPermission(userUri, Binder.getCallingPid(),
                             callingUid, Intent.FLAG_GRANT_READ_URI_PERMISSION)
-                            != PackageManager.PERMISSION_GRANTED
-                            && !deniedAccessSystemUserOnlyProvider(callingUserId,
-                            mSystemUserOnly)) {
+                            != PackageManager.PERMISSION_GRANTED) {
                         FrameworkStatsLog.write(GET_TYPE_ACCESSED_WITHOUT_PERMISSION,
                                 enumCheckUriPermission,
                                 callingUid, uri.getAuthority(), type);
@@ -869,10 +865,6 @@
     boolean checkUser(int pid, int uid, Context context) {
         final int callingUserId = UserHandle.getUserId(uid);
 
-        if (deniedAccessSystemUserOnlyProvider(callingUserId, mSystemUserOnly)) {
-            return false;
-        }
-
         if (callingUserId == context.getUserId() || mSingleUser) {
             return true;
         }
@@ -995,9 +987,6 @@
 
         // last chance, check against any uri grants
         final int callingUserId = UserHandle.getUserId(uid);
-        if (deniedAccessSystemUserOnlyProvider(callingUserId, mSystemUserOnly)) {
-            return PermissionChecker.PERMISSION_HARD_DENIED;
-        }
         final Uri userUri = (mSingleUser && !UserHandle.isSameUser(mMyUid, uid))
                 ? maybeAddUserId(uri, callingUserId) : uri;
         if (context.checkUriPermission(userUri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION)
@@ -2634,7 +2623,6 @@
                 setPathPermissions(info.pathPermissions);
                 mExported = info.exported;
                 mSingleUser = (info.flags & ProviderInfo.FLAG_SINGLE_USER) != 0;
-                mSystemUserOnly = (info.flags & ProviderInfo.FLAG_SYSTEM_USER_ONLY) != 0;
                 setAuthorities(info.authority);
             }
             if (Build.IS_DEBUGGABLE) {
@@ -2768,11 +2756,6 @@
         String auth = uri.getAuthority();
         if (!mSingleUser) {
             int userId = getUserIdFromAuthority(auth, UserHandle.USER_CURRENT);
-            if (deniedAccessSystemUserOnlyProvider(mContext.getUserId(),
-                    mSystemUserOnly)) {
-                throw new SecurityException("Trying to query a SYSTEM user only content"
-                        + " provider from user:" + mContext.getUserId());
-            }
             if (userId != UserHandle.USER_CURRENT
                     && userId != mContext.getUserId()
                     // Since userId specified in content uri, the provider userId would be
@@ -2946,16 +2929,4 @@
             Trace.traceBegin(traceTag, methodName + subInfo);
         }
     }
-    /**
-     * Return true if access to content provider is denied because it's a SYSTEM user only
-     * provider and the calling user is not the SYSTEM user.
-     *
-     * @param callingUserId UserId of the caller accessing the content provider.
-     * @param systemUserOnly true when the content provider is only available for the SYSTEM user.
-     */
-    private static boolean deniedAccessSystemUserOnlyProvider(int callingUserId,
-            boolean systemUserOnly) {
-        return Flags.enableSystemUserOnlyForServicesAndProviders()
-                && (callingUserId != UserHandle.USER_SYSTEM && systemUserOnly);
-    }
 }
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index fa76e39..4bc1237 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -16,6 +16,8 @@
 
 package android.content;
 
+import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS;
+
 import android.annotation.AttrRes;
 import android.annotation.CallbackExecutor;
 import android.annotation.CheckResult;
@@ -296,6 +298,7 @@
             BIND_ALLOW_ACTIVITY_STARTS,
             BIND_INCLUDE_CAPABILITIES,
             BIND_SHARED_ISOLATED_PROCESS,
+            BIND_PACKAGE_ISOLATED_PROCESS,
             BIND_EXTERNAL_SERVICE
     })
     @Retention(RetentionPolicy.SOURCE)
@@ -318,6 +321,7 @@
             BIND_ALLOW_ACTIVITY_STARTS,
             BIND_INCLUDE_CAPABILITIES,
             BIND_SHARED_ISOLATED_PROCESS,
+            BIND_PACKAGE_ISOLATED_PROCESS,
             // Intentionally not include BIND_EXTERNAL_SERVICE, because it'd cause sign-extension.
             // This would allow Android Studio to show a warning, if someone tries to use
             // BIND_EXTERNAL_SERVICE BindServiceFlags.
@@ -511,6 +515,26 @@
      */
     public static final int BIND_SHARED_ISOLATED_PROCESS = 0x00002000;
 
+    /**
+     * Flag for {@link #bindIsolatedService}: Bind the service into a shared isolated process,
+     * but only with other isolated services from the same package that declare the same process
+     * name.
+     *
+     * <p>Specifying this flag allows multiple isolated services defined in the same package to be
+     * running in a single shared isolated process. This shared isolated process must be specified
+     * since this flag will not work with the default application process.
+     *
+     * <p>This flag is different from {@link #BIND_SHARED_ISOLATED_PROCESS} since it only
+     * allows binding services from the same package in the same shared isolated process. This also
+     * means the shared package isolated process is global, and not scoped to each potential
+     * calling app.
+     *
+     * <p>The shared isolated process instance is identified by the "android:process" attribute
+     * defined by the service. This flag cannot be used without this attribute set.
+     */
+    @FlaggedApi(FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS)
+    public static final int BIND_PACKAGE_ISOLATED_PROCESS = 1 << 14;
+
     /***********    Public flags above this line ***********/
     /***********    Hidden flags below this line ***********/
 
@@ -6734,7 +6758,7 @@
             @Intent.AccessUriMode int modeFlags);
 
     /**
-     * Determine whether a particular process and user ID has been granted
+     * Determine whether a particular process and uid has been granted
      * permission to access a specific URI.  This only checks for permissions
      * that have been explicitly granted -- if the given process/uid has
      * more general access to the URI's content provider then this check will
@@ -6758,7 +6782,38 @@
             @Intent.AccessUriMode int modeFlags);
 
     /**
-     * Determine whether a particular process and user ID has been granted
+     * Determine whether a particular process and uid has been granted
+     * permission to access a specific content URI.
+     *
+     * <p>Unlike {@link #checkUriPermission(Uri, int, int, int)}, this method
+     * checks for general access to the URI's content provider, as well as
+     * explicitly granted permissions.</p>
+     *
+     * <p>Note, this check will throw an {@link IllegalArgumentException}
+     * for non-content URIs.</p>
+     *
+     * @param uri The content uri that is being checked.
+     * @param pid (Optional) The process ID being checked against. If the
+     * pid is unknown, pass -1.
+     * @param uid The UID being checked against.  A uid of 0 is the root
+     * user, which will pass every permission check.
+     * @param modeFlags The access modes to check.
+     *
+     * @return {@link PackageManager#PERMISSION_GRANTED} if the given
+     * pid/uid is allowed to access that uri, or
+     * {@link PackageManager#PERMISSION_DENIED} if it is not.
+     *
+     * @see #checkUriPermission(Uri, int, int, int)
+     */
+    @FlaggedApi(android.security.Flags.FLAG_CONTENT_URI_PERMISSION_APIS)
+    @PackageManager.PermissionResult
+    public int checkContentUriPermissionFull(@NonNull Uri uri, int pid, int uid,
+            @Intent.AccessUriMode int modeFlags) {
+        throw new RuntimeException("Not implemented. Must override in a subclass.");
+    }
+
+    /**
+     * Determine whether a particular process and uid has been granted
      * permission to access a list of URIs.  This only checks for permissions
      * that have been explicitly granted -- if the given process/uid has
      * more general access to the URI's content provider then this check will
@@ -6794,7 +6849,7 @@
             @Intent.AccessUriMode int modeFlags, IBinder callerToken);
 
     /**
-     * Determine whether the calling process and user ID has been
+     * Determine whether the calling process and uid has been
      * granted permission to access a specific URI.  This is basically
      * the same as calling {@link #checkUriPermission(Uri, int, int,
      * int)} with the pid and uid returned by {@link
@@ -6817,7 +6872,7 @@
     public abstract int checkCallingUriPermission(Uri uri, @Intent.AccessUriMode int modeFlags);
 
     /**
-     * Determine whether the calling process and user ID has been
+     * Determine whether the calling process and uid has been
      * granted permission to access a list of URIs.  This is basically
      * the same as calling {@link #checkUriPermissions(List, int, int, int)}
      * with the pid and uid returned by {@link
@@ -6911,7 +6966,7 @@
             @Intent.AccessUriMode int modeFlags);
 
     /**
-     * If a particular process and user ID has not been granted
+     * If a particular process and uid has not been granted
      * permission to access a specific URI, throw {@link
      * SecurityException}.  This only checks for permissions that have
      * been explicitly granted -- if the given process/uid has more
@@ -6931,7 +6986,7 @@
             Uri uri, int pid, int uid, @Intent.AccessUriMode int modeFlags, String message);
 
     /**
-     * If the calling process and user ID has not been granted
+     * If the calling process and uid has not been granted
      * permission to access a specific URI, throw {@link
      * SecurityException}.  This is basically the same as calling
      * {@link #enforceUriPermission(Uri, int, int, int, String)} with
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index 0a8029c..e0cf0a5 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -17,6 +17,7 @@
 package android.content;
 
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -1003,6 +1004,12 @@
         return mBase.checkUriPermission(uri, pid, uid, modeFlags);
     }
 
+    @FlaggedApi(android.security.Flags.FLAG_CONTENT_URI_PERMISSION_APIS)
+    @Override
+    public int checkContentUriPermissionFull(@NonNull Uri uri, int pid, int uid, int modeFlags) {
+        return mBase.checkContentUriPermissionFull(uri, pid, uid, modeFlags);
+    }
+
     @NonNull
     @Override
     public int[] checkUriPermissions(@NonNull List<Uri> uris, int pid, int uid,
diff --git a/core/java/android/content/flags/flags.aconfig b/core/java/android/content/flags/flags.aconfig
new file mode 100644
index 0000000..3445fb5
--- /dev/null
+++ b/core/java/android/content/flags/flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.content.flags"
+
+flag {
+    name: "enable_bind_package_isolated_process"
+    namespace: "machine_learning"
+    description: "This flag enables the newly added flag for binding package-private isolated processes."
+    bug: "312706530"
+}
\ No newline at end of file
diff --git a/core/java/android/content/pm/ProviderInfo.java b/core/java/android/content/pm/ProviderInfo.java
index de33fa8..9e553db 100644
--- a/core/java/android/content/pm/ProviderInfo.java
+++ b/core/java/android/content/pm/ProviderInfo.java
@@ -89,15 +89,6 @@
     public static final int FLAG_VISIBLE_TO_INSTANT_APP = 0x100000;
 
     /**
-     * Bit in {@link #flags}: If set, this provider will only be available
-     * for the system user.
-     * Set from the android.R.attr#systemUserOnly attribute.
-     * In Sync with {@link ActivityInfo#FLAG_SYSTEM_USER_ONLY}
-     * @hide
-     */
-    public static final int FLAG_SYSTEM_USER_ONLY = ActivityInfo.FLAG_SYSTEM_USER_ONLY;
-
-    /**
      * Bit in {@link #flags}: If set, a single instance of the provider will
      * run for all users on the device.  Set from the
      * {@link android.R.attr#singleUser} attribute.
diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java
index 2b378b1..ae46c027 100644
--- a/core/java/android/content/pm/ServiceInfo.java
+++ b/core/java/android/content/pm/ServiceInfo.java
@@ -101,14 +101,6 @@
     public static final int FLAG_VISIBLE_TO_INSTANT_APP = 0x100000;
 
     /**
-     * @hide Bit in {@link #flags}: If set, this service will only be available
-     * for the system user.
-     * Set from the android.R.attr#systemUserOnly attribute.
-     * In Sync with {@link ActivityInfo#FLAG_SYSTEM_USER_ONLY}
-     */
-    public static final int FLAG_SYSTEM_USER_ONLY = ActivityInfo.FLAG_SYSTEM_USER_ONLY;
-
-    /**
      * Bit in {@link #flags}: If set, a single instance of the service will
      * run for all users on the device.  Set from the
      * {@link android.R.attr#singleUser} attribute.
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 1036865..c7797c7 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -70,11 +70,4 @@
     namespace: "profile_experiences"
     description: "Add support for Private Space in resolver sheet"
     bug: "307515485"
-}
-flag {
-    name: "enable_system_user_only_for_services_and_providers"
-    namespace: "multiuser"
-    description: "Enable systemUserOnly manifest attribute for services and providers."
-    bug: "302354856"
-    is_fixed_read_only: true
-}
+}
\ No newline at end of file
diff --git a/core/java/android/tracing/transition/TransitionDataSource.java b/core/java/android/tracing/transition/TransitionDataSource.java
new file mode 100644
index 0000000..b8daace
--- /dev/null
+++ b/core/java/android/tracing/transition/TransitionDataSource.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tracing.transition;
+
+import android.tracing.perfetto.DataSource;
+import android.tracing.perfetto.DataSourceInstance;
+import android.tracing.perfetto.FlushCallbackArguments;
+import android.tracing.perfetto.StartCallbackArguments;
+import android.tracing.perfetto.StopCallbackArguments;
+import android.util.proto.ProtoInputStream;
+
+/**
+ * @hide
+ */
+public class TransitionDataSource extends DataSource {
+    public static String DATA_SOURCE_NAME = "com.android.wm.shell.transition";
+
+    private final Runnable mOnStartStaticCallback;
+    private final Runnable mOnFlushStaticCallback;
+    private final Runnable mOnStopStaticCallback;
+
+    public TransitionDataSource(Runnable onStart, Runnable onFlush, Runnable onStop) {
+        super(DATA_SOURCE_NAME);
+        this.mOnStartStaticCallback = onStart;
+        this.mOnFlushStaticCallback = onFlush;
+        this.mOnStopStaticCallback = onStop;
+    }
+
+    @Override
+    public DataSourceInstance createInstance(ProtoInputStream configStream, int instanceIndex) {
+        return new DataSourceInstance(this, instanceIndex) {
+            @Override
+            protected void onStart(StartCallbackArguments args) {
+                mOnStartStaticCallback.run();
+            }
+
+            @Override
+            protected void onFlush(FlushCallbackArguments args) {
+                mOnFlushStaticCallback.run();
+            }
+
+            @Override
+            protected void onStop(StopCallbackArguments args) {
+                mOnStopStaticCallback.run();
+            }
+        };
+    }
+}
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
index 12aff1c..5d82d04 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
@@ -29,7 +29,6 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
-import android.multiuser.Flags;
 import android.os.Build;
 import android.os.PatternMatcher;
 import android.util.Slog;
@@ -127,10 +126,6 @@
                     .setFlags(provider.getFlags() | flag(ProviderInfo.FLAG_SINGLE_USER,
                             R.styleable.AndroidManifestProvider_singleUser, sa));
 
-            if (Flags.enableSystemUserOnlyForServicesAndProviders()) {
-                provider.setFlags(provider.getFlags() | flag(ProviderInfo.FLAG_SYSTEM_USER_ONLY,
-                        R.styleable.AndroidManifestProvider_systemUserOnly, sa));
-            }
             visibleToEphemeral = sa.getBoolean(
                     R.styleable.AndroidManifestProvider_visibleToInstantApps, false);
             if (visibleToEphemeral) {
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
index 4ac542f8..a1dd19a3 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
@@ -29,7 +29,6 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
-import android.multiuser.Flags;
 import android.os.Build;
 
 import com.android.internal.R;
@@ -106,11 +105,6 @@
                             | flag(ServiceInfo.FLAG_SINGLE_USER,
                             R.styleable.AndroidManifestService_singleUser, sa)));
 
-            if (Flags.enableSystemUserOnlyForServicesAndProviders()) {
-                service.setFlags(service.getFlags() | flag(ServiceInfo.FLAG_SYSTEM_USER_ONLY,
-                        R.styleable.AndroidManifestService_systemUserOnly, sa));
-            }
-
             visibleToEphemeral = sa.getBoolean(
                     R.styleable.AndroidManifestService_visibleToInstantApps, false);
             if (visibleToEphemeral) {
diff --git a/core/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java
index 58ee2b2..13efaf0 100644
--- a/core/java/com/android/internal/util/LatencyTracker.java
+++ b/core/java/com/android/internal/util/LatencyTracker.java
@@ -26,6 +26,8 @@
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FACE_WAKE_AND_UNLOCK;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FINGERPRINT_WAKE_AND_UNLOCK;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FOLD_TO_AOD;
+import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE;
+import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOAD_SHARE_SHEET;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOCKSCREEN_UNLOCK;
@@ -234,6 +236,19 @@
      */
     public static final int ACTION_BACK_SYSTEM_ANIMATION = 25;
 
+    /**
+     * Time notifications spent in hidden state for performance reasons. We might temporary
+     * hide notifications after display size changes (e.g. fold/unfold of a foldable device)
+     * and measure them while they are hidden to unblock rendering of the rest of the UI.
+     */
+    public static final int ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE = 26;
+
+    /**
+     * The same as {@link ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE} but tracks time only
+     * when the notifications are hidden and when the shade is open or keyguard is visible.
+     */
+    public static final int ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN = 27;
+
     private static final int[] ACTIONS_ALL = {
         ACTION_EXPAND_PANEL,
         ACTION_TOGGLE_RECENTS,
@@ -261,6 +276,8 @@
         ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
         ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME,
         ACTION_BACK_SYSTEM_ANIMATION,
+        ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE,
+        ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN,
     };
 
     /** @hide */
@@ -291,6 +308,8 @@
         ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
         ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME,
         ACTION_BACK_SYSTEM_ANIMATION,
+        ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE,
+        ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Action {
@@ -324,6 +343,8 @@
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME,
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_BACK_SYSTEM_ANIMATION,
+            UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE,
+            UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN,
     };
 
     private final Object mLock = new Object();
@@ -514,6 +535,10 @@
                 return "ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME";
             case UIACTION_LATENCY_REPORTED__ACTION__ACTION_BACK_SYSTEM_ANIMATION:
                 return "ACTION_BACK_SYSTEM_ANIMATION";
+            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE:
+                return "ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE";
+            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN:
+                return "ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN";
             default:
                 throw new IllegalArgumentException("Invalid action");
         }
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 6019524..8fae6db 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -506,12 +506,6 @@
          receivers, and providers; it can not be used with activities. -->
     <attr name="singleUser" format="boolean" />
 
-    <!-- If set to true, only a single instance of this component will
-    run and be available for the SYSTEM user. Non SYSTEM users will not be
-    allowed to access the component if this flag is enabled.
-    This flag can be used with services, receivers, providers and activities. -->
-    <attr name="systemUserOnly" format="boolean" />
-
     <!-- Specify a specific process that the associated code is to run in.
          Use with the application tag (to supply a default process for all
          application components), or with the activity, receiver, service,
@@ -2865,7 +2859,6 @@
              Context.createAttributionContext() using the first attribution tag
              contained here. -->
         <attr name="attributionTags" />
-        <attr name="systemUserOnly" format="boolean" />
     </declare-styleable>
 
     <!-- Attributes that can be supplied in an AndroidManifest.xml
@@ -3024,7 +3017,6 @@
              ignored when the process is bound into a shared isolated process by a client.
         -->
         <attr name="allowSharedIsolatedProcess" format="boolean" />
-        <attr name="systemUserOnly" format="boolean" />
     </declare-styleable>
 
     <!-- @hide The <code>apex-system-service</code> tag declares an apex system service
@@ -3152,7 +3144,7 @@
         <attr name="uiOptions" />
         <attr name="parentActivityName" />
         <attr name="singleUser" />
-        <!-- This broadcast receiver or activity will only receive broadcasts for the
+        <!-- @hide This broadcast receiver or activity will only receive broadcasts for the
              system user-->
         <attr name="systemUserOnly" format="boolean" />
         <attr name="persistableMode" />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 5be29a6..28adccd 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -57,6 +57,7 @@
         <item><xliff:g id="id">@string/status_bar_screen_record</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_cast</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_ethernet</xliff:g></item>
+        <item><xliff:g id="id">@string/status_bar_oem_satellite</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_wifi</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_hotspot</xliff:g></item>
         <item><xliff:g id="id">@string/status_bar_mobile</xliff:g></item>
@@ -102,6 +103,7 @@
     <string translatable="false" name="status_bar_call_strength">call_strength</string>
     <string translatable="false" name="status_bar_sensors_off">sensors_off</string>
     <string translatable="false" name="status_bar_screen_record">screen_record</string>
+    <string translatable="false" name="status_bar_oem_satellite">satellite</string>
 
     <!-- Flag indicating whether the surface flinger has limited
          alpha compositing functionality in hardware.  If set, the window
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index 7b5c49c..53b473e 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -119,8 +119,6 @@
     <public name="optional"/>
     <!-- @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") -->
     <public name="adServiceTypes" />
-    <!-- @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") -->
-    <public name="systemUserOnly"/>
   </staging-public-group>
 
   <staging-public-group type="id" first-id="0x01bc0000">
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index d12ef2b..ef12d8f 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3198,6 +3198,7 @@
   <java-symbol type="string" name="status_bar_camera" />
   <java-symbol type="string" name="status_bar_sensors_off" />
   <java-symbol type="string" name="status_bar_screen_record" />
+  <java-symbol type="string" name="status_bar_oem_satellite" />
 
   <!-- Locale picker -->
   <java-symbol type="id" name="locale_search_menu" />
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
index b5af845..9af799c 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
@@ -31,7 +31,7 @@
 import android.os.Process;
 import android.util.Log;
 
-import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -105,7 +105,7 @@
         }
     }
 
-    @Nullable
+    @NonNull
     private String[] getRequestedPermissions(String callingPackage) {
         String[] requestedPermissions = null;
         try {
@@ -115,7 +115,7 @@
             // Should be unreachable because we've just fetched the packageName above.
             Log.e(TAG, "Package not found for " + callingPackage);
         }
-        return requestedPermissions;
+        return requestedPermissions == null ? new String[]{} : requestedPermissions;
     }
 
     void startUnarchive() {
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 536ddc7..a4b3af9 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -338,6 +338,9 @@
     <!-- Message for telling the user the kind of BT device being displayed in list. [CHAR LIMIT=30 BACKUP_MESSAGE_ID=5165842622743212268] -->
     <string name="bluetooth_talkback_input_peripheral">Input Peripheral</string>
 
+    <!-- Message for telling the user the kind of BT device being displayed in list. [CHAR LIMIT=30 BACKUP_MESSAGE_ID=26580326066627664] -->
+    <string name="bluetooth_talkback_hearing_aids">Hearing Aids</string>
+
     <!-- Message for telling the user the kind of BT device being displayed in list. [CHAR LIMIT=30 BACKUP_MESSAGE_ID=5615463912185280812] -->
     <string name="bluetooth_talkback_bluetooth">Bluetooth</string>
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 0ffcc45..071afaf 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -125,7 +125,8 @@
                 // The device should show hearing aid icon if it contains any hearing aid related
                 // profiles
                 if (profile instanceof HearingAidProfile || profile instanceof HapClientProfile) {
-                    return new Pair<>(getBluetoothDrawable(context, profileResId), null);
+                    return new Pair<>(getBluetoothDrawable(context, profileResId),
+                            context.getString(R.string.bluetooth_talkback_hearing_aids));
                 }
                 if (resId == 0) {
                     resId = profileResId;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 245fe6e..032a838 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -410,10 +410,14 @@
         connectDevice();
     }
 
-    void setHearingAidInfo(HearingAidInfo hearingAidInfo) {
+    public void setHearingAidInfo(HearingAidInfo hearingAidInfo) {
         mHearingAidInfo = hearingAidInfo;
     }
 
+    public HearingAidInfo getHearingAidInfo() {
+        return mHearingAidInfo;
+    }
+
     /**
      * @return {@code true} if {@code cachedBluetoothDevice} is hearing aid device
      */
@@ -1788,4 +1792,40 @@
     boolean getUnpairing() {
         return mUnpairing;
     }
+
+    ListenableFuture<Void> syncProfileForMemberDevice() {
+        return ThreadUtils.getBackgroundExecutor()
+            .submit(
+                () -> {
+                    List<Pair<LocalBluetoothProfile, Boolean>> toSync =
+                        Stream.of(
+                            mProfileManager.getA2dpProfile(),
+                            mProfileManager.getHeadsetProfile(),
+                            mProfileManager.getHearingAidProfile(),
+                            mProfileManager.getLeAudioProfile(),
+                            mProfileManager.getLeAudioBroadcastAssistantProfile())
+                        .filter(Objects::nonNull)
+                        .map(profile -> new Pair<>(profile, profile.isEnabled(mDevice)))
+                        .toList();
+
+                    for (var t : toSync) {
+                        LocalBluetoothProfile profile = t.first;
+                        boolean enabledForMain = t.second;
+
+                        for (var member : mMemberDevices) {
+                            BluetoothDevice btDevice = member.getDevice();
+
+                            if (enabledForMain != profile.isEnabled(btDevice)) {
+                                Log.d(TAG, "Syncing profile " + profile + " to "
+                                        + enabledForMain + " for member device "
+                                        + btDevice.getAnonymizedAddress() + " of main device "
+                                        + mDevice.getAnonymizedAddress());
+                                profile.setEnabled(btDevice, enabledForMain);
+                            }
+                        }
+                    }
+                    return null;
+                }
+            );
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index a6536a8c..89fe268 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -349,6 +349,7 @@
         if (profileId == BluetoothProfile.HEADSET
                 || profileId == BluetoothProfile.A2DP
                 || profileId == BluetoothProfile.LE_AUDIO
+                || profileId == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
                 || profileId == BluetoothProfile.CSIP_SET_COORDINATOR) {
             return mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice,
                 state);
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
index a49314a..e67ec48 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
@@ -379,6 +379,7 @@
         if (hasChanged) {
             log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: "
                     + mCachedDevices);
+            preferredMainDevice.syncProfileForMemberDevice();
         }
         return hasChanged;
     }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
index ed545ab..9db8b47 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
@@ -18,7 +18,9 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -56,6 +58,8 @@
 import org.robolectric.annotation.Config;
 import org.robolectric.shadow.api.Shadow;
 
+import java.util.concurrent.ExecutionException;
+
 @RunWith(RobolectricTestRunner.class)
 @Config(shadows = {ShadowBluetoothAdapter.class})
 public class CachedBluetoothDeviceTest {
@@ -1815,6 +1819,52 @@
         assertThat(mCachedDevice.isConnectedHearingAidDevice()).isFalse();
     }
 
+    @Test
+    public void syncProfileForMemberDevice_hasDiff_shouldSync()
+            throws ExecutionException, InterruptedException {
+        mCachedDevice.addMemberDevice(mSubCachedDevice);
+        when(mProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile);
+        when(mProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile);
+        when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile);
+
+        when(mA2dpProfile.isEnabled(mDevice)).thenReturn(true);
+        when(mHearingAidProfile.isEnabled(mDevice)).thenReturn(true);
+        when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(true);
+
+        when(mA2dpProfile.isEnabled(mSubDevice)).thenReturn(true);
+        when(mHearingAidProfile.isEnabled(mSubDevice)).thenReturn(false);
+        when(mLeAudioProfile.isEnabled(mSubDevice)).thenReturn(false);
+
+        mCachedDevice.syncProfileForMemberDevice().get();
+
+        verify(mA2dpProfile, never()).setEnabled(any(BluetoothDevice.class), anyBoolean());
+        verify(mHearingAidProfile).setEnabled(any(BluetoothDevice.class), eq(true));
+        verify(mLeAudioProfile).setEnabled(any(BluetoothDevice.class), eq(true));
+    }
+
+    @Test
+    public void syncProfileForMemberDevice_noDiff_shouldNotSync()
+            throws ExecutionException, InterruptedException {
+        mCachedDevice.addMemberDevice(mSubCachedDevice);
+        when(mProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile);
+        when(mProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile);
+        when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile);
+
+        when(mA2dpProfile.isEnabled(mDevice)).thenReturn(false);
+        when(mHearingAidProfile.isEnabled(mDevice)).thenReturn(false);
+        when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(true);
+
+        when(mA2dpProfile.isEnabled(mSubDevice)).thenReturn(false);
+        when(mHearingAidProfile.isEnabled(mSubDevice)).thenReturn(false);
+        when(mLeAudioProfile.isEnabled(mSubDevice)).thenReturn(true);
+
+        mCachedDevice.syncProfileForMemberDevice().get();
+
+        verify(mA2dpProfile, never()).setEnabled(any(BluetoothDevice.class), anyBoolean());
+        verify(mHearingAidProfile, never()).setEnabled(any(BluetoothDevice.class), anyBoolean());
+        verify(mLeAudioProfile, never()).setEnabled(any(BluetoothDevice.class), anyBoolean());
+    }
+
     private HearingAidInfo getLeftAshaHearingAidInfo() {
         return new HearingAidInfo.Builder()
                 .setAshaDeviceSide(HearingAidProfile.DeviceSide.SIDE_LEFT)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
index 84d4246..56d6879 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -17,13 +17,16 @@
 package com.android.systemui.keyguard.ui.composable.blueprint
 
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.IntRect
 import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.padding
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
 import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
@@ -66,6 +69,7 @@
     override fun SceneScope.Content(modifier: Modifier) {
         val isUdfpsVisible = viewModel.isUdfpsVisible
         val burnIn = rememberBurnIn(clockInteractor)
+        val resources = LocalContext.current.resources
 
         LockscreenLongPress(
             viewModel = viewModel.longPress,
@@ -88,13 +92,27 @@
                             SmartSpace(
                                 burnInParams = burnIn.parameters,
                                 onTopChanged = burnIn.onSmartspaceTopChanged,
-                                modifier = Modifier.fillMaxWidth(),
+                                modifier =
+                                    Modifier.fillMaxWidth()
+                                        .padding(
+                                            top = { viewModel.getSmartSpacePaddingTop(resources) }
+                                        ),
                             )
                         }
-                        with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
-                        with(notificationSection) {
-                            Notifications(modifier = Modifier.fillMaxWidth().weight(1f))
+
+                        if (viewModel.isLargeClockVisible) {
+                            Spacer(modifier = Modifier.weight(weight = 1f))
+                            with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
                         }
+
+                        if (viewModel.areNotificationsVisible) {
+                            with(notificationSection) {
+                                Notifications(
+                                    modifier = Modifier.fillMaxWidth().weight(weight = 1f)
+                                )
+                            }
+                        }
+
                         if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
                             with(ambientIndicationSectionOptional.get()) {
                                 AmbientIndication(modifier = Modifier.fillMaxWidth())
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
index 4148462..d0aa444 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
@@ -17,13 +17,16 @@
 package com.android.systemui.keyguard.ui.composable.blueprint
 
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.IntRect
 import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.padding
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
 import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
@@ -66,6 +69,7 @@
     override fun SceneScope.Content(modifier: Modifier) {
         val isUdfpsVisible = viewModel.isUdfpsVisible
         val burnIn = rememberBurnIn(clockInteractor)
+        val resources = LocalContext.current.resources
 
         LockscreenLongPress(
             viewModel = viewModel.longPress,
@@ -88,13 +92,27 @@
                             SmartSpace(
                                 burnInParams = burnIn.parameters,
                                 onTopChanged = burnIn.onSmartspaceTopChanged,
-                                modifier = Modifier.fillMaxWidth(),
+                                modifier =
+                                    Modifier.fillMaxWidth()
+                                        .padding(
+                                            top = { viewModel.getSmartSpacePaddingTop(resources) }
+                                        ),
                             )
                         }
-                        with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
-                        with(notificationSection) {
-                            Notifications(modifier = Modifier.fillMaxWidth().weight(1f))
+
+                        if (viewModel.isLargeClockVisible) {
+                            Spacer(modifier = Modifier.weight(weight = 1f))
+                            with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
                         }
+
+                        if (viewModel.areNotificationsVisible) {
+                            with(notificationSection) {
+                                Notifications(
+                                    modifier = Modifier.fillMaxWidth().weight(weight = 1f)
+                                )
+                            }
+                        }
+
                         if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
                             with(ambientIndicationSectionOptional.get()) {
                                 AmbientIndication(modifier = Modifier.fillMaxWidth())
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
index f021bb6..f40b871 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
@@ -30,10 +30,10 @@
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.modifiers.padding
 import com.android.keyguard.KeyguardClockSwitch
+import com.android.systemui.customization.R as customizationR
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChanged
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
-import com.android.systemui.res.R
 import javax.inject.Inject
 
 class ClockSection
@@ -79,7 +79,7 @@
                     modifier =
                         Modifier.padding(
                                 horizontal =
-                                    dimensionResource(R.dimen.keyguard_affordance_horizontal_offset)
+                                    dimensionResource(customizationR.dimen.clock_padding_start)
                             )
                             .padding(top = { viewModel.getSmallClockTopMargin(view.context) })
                             .onTopPlacementChanged(onTopChanged),
@@ -117,9 +117,7 @@
             content {
                 AndroidView(
                     factory = { checkNotNull(currentClock).largeClock.view },
-                    modifier =
-                        Modifier.fillMaxWidth()
-                            .padding(top = { viewModel.getLargeClockTopMargin(view.context) })
+                    modifier = Modifier.fillMaxWidth()
                 )
             }
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
new file mode 100644
index 0000000..d4dd2ac
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardClockSwitch
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.authController
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LockscreenContentViewModelTest : SysuiTestCase() {
+
+    private val kosmos: Kosmos = testKosmos()
+
+    lateinit var underTest: LockscreenContentViewModel
+
+    @Before
+    fun setup() {
+        with(kosmos) {
+            fakeFeatureFlagsClassic.set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, true)
+            underTest = lockscreenContentViewModel
+        }
+    }
+
+    @Test
+    fun isUdfpsVisible_withUdfps_true() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(kosmos.authController.isUdfpsSupported).thenReturn(true)
+                assertThat(underTest.isUdfpsVisible).isTrue()
+            }
+        }
+
+    @Test
+    fun isUdfpsVisible_withoutUdfps_false() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(kosmos.authController.isUdfpsSupported).thenReturn(false)
+                assertThat(underTest.isUdfpsVisible).isFalse()
+            }
+        }
+
+    @Test
+    fun isLargeClockVisible_withLargeClock_true() =
+        with(kosmos) {
+            testScope.runTest {
+                kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.LARGE)
+                assertThat(underTest.isLargeClockVisible).isTrue()
+            }
+        }
+
+    @Test
+    fun isLargeClockVisible_withSmallClock_false() =
+        with(kosmos) {
+            testScope.runTest {
+                kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.SMALL)
+                assertThat(underTest.isLargeClockVisible).isFalse()
+            }
+        }
+
+    @Test
+    fun areNotificationsVisible_withSmallClock_true() =
+        with(kosmos) {
+            testScope.runTest {
+                kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.SMALL)
+                assertThat(underTest.areNotificationsVisible).isTrue()
+            }
+        }
+
+    @Test
+    fun areNotificationsVisible_withLargeClock_false() =
+        with(kosmos) {
+            testScope.runTest {
+                kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.LARGE)
+                assertThat(underTest.areNotificationsVisible).isFalse()
+            }
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
index 070e07a..eb845b2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
@@ -17,11 +17,9 @@
 package com.android.systemui.qs.pipeline.data.repository
 
 import android.Manifest.permission.BIND_QUICK_SETTINGS_TILE
-import android.content.BroadcastReceiver
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
-import android.content.IntentFilter
 import android.content.pm.ApplicationInfo
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.ResolveInfoFlags
@@ -33,44 +31,36 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.data.repository.fakePackageChangeRepository
+import com.android.systemui.common.data.repository.packageChangeRepository
+import com.android.systemui.common.data.shared.model.PackageChangeModel
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argThat
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.StandardTestDispatcher
-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.ArgumentCaptor
 import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Captor
 import org.mockito.Mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
-@OptIn(ExperimentalCoroutinesApi::class)
 class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() {
-    private val testDispatcher = StandardTestDispatcher()
-    private val testScope = TestScope(testDispatcher)
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
 
     @Mock private lateinit var context: Context
     @Mock private lateinit var packageManager: PackageManager
-    @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver>
 
     private lateinit var underTest: InstalledTilesComponentRepositoryImpl
 
@@ -92,63 +82,12 @@
         underTest =
             InstalledTilesComponentRepositoryImpl(
                 context,
-                testDispatcher,
+                kosmos.testDispatcher,
+                kosmos.packageChangeRepository
             )
     }
 
     @Test
-    fun registersAndUnregistersBroadcastReceiver() =
-        testScope.runTest {
-            val user = 10
-            val job = launch { underTest.getInstalledTilesComponents(user).collect {} }
-            runCurrent()
-
-            verify(context)
-                .registerReceiverAsUser(
-                    capture(receiverCaptor),
-                    eq(UserHandle.of(user)),
-                    any(),
-                    nullable(),
-                    nullable(),
-                )
-
-            verify(context, never()).unregisterReceiver(receiverCaptor.value)
-
-            job.cancel()
-            runCurrent()
-            verify(context).unregisterReceiver(receiverCaptor.value)
-        }
-
-    @Test
-    fun intentFilterForCorrectActionsAndScheme() =
-        testScope.runTest {
-            val filterCaptor = argumentCaptor<IntentFilter>()
-
-            backgroundScope.launch { underTest.getInstalledTilesComponents(0).collect {} }
-            runCurrent()
-
-            verify(context)
-                .registerReceiverAsUser(
-                    any(),
-                    any(),
-                    capture(filterCaptor),
-                    nullable(),
-                    nullable(),
-                )
-
-            with(filterCaptor.value) {
-                assertThat(matchAction(Intent.ACTION_PACKAGE_CHANGED)).isTrue()
-                assertThat(matchAction(Intent.ACTION_PACKAGE_ADDED)).isTrue()
-                assertThat(matchAction(Intent.ACTION_PACKAGE_REMOVED)).isTrue()
-                assertThat(matchAction(Intent.ACTION_PACKAGE_REPLACED)).isTrue()
-                assertThat(countActions()).isEqualTo(4)
-
-                assertThat(hasDataScheme("package")).isTrue()
-                assertThat(countDataSchemes()).isEqualTo(1)
-            }
-        }
-
-    @Test
     fun componentsLoadedOnStart() =
         testScope.runTest {
             val userId = 0
@@ -169,7 +108,7 @@
         }
 
     @Test
-    fun componentAdded_foundAfterBroadcast() =
+    fun componentAdded_foundAfterPackageChange() =
         testScope.runTest {
             val userId = 0
             val resolveInfo =
@@ -186,7 +125,7 @@
                     )
                 )
                 .thenReturn(listOf(resolveInfo))
-            getRegisteredReceiver().onReceive(context, Intent(Intent.ACTION_PACKAGE_ADDED))
+            kosmos.fakePackageChangeRepository.notifyChange(PackageChangeModel.Empty)
 
             assertThat(componentNames).containsExactly(TEST_COMPONENT)
         }
@@ -275,19 +214,6 @@
             assertThat(componentNames).containsExactly(TEST_COMPONENT)
         }
 
-    private fun getRegisteredReceiver(): BroadcastReceiver {
-        verify(context)
-            .registerReceiverAsUser(
-                capture(receiverCaptor),
-                any(),
-                any(),
-                nullable(),
-                nullable(),
-            )
-
-        return receiverCaptor.value
-    }
-
     companion object {
         private val INTENT = Intent(TileService.ACTION_QS_TILE)
         private val FLAGS =
diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml
new file mode 100644
index 0000000..045c19e
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z"
+        android:fillAlpha="0.3"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z"
+        android:fillAlpha="0.3"
+        android:fillColor="#fff"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml
new file mode 100644
index 0000000..5e012ab
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z"
+        android:fillAlpha="0.3"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z"
+        android:fillColor="#fff"/>
+</vector>
+
diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml
new file mode 100644
index 0000000..d8a9a70
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z"
+        android:fillColor="#fff"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml
new file mode 100644
index 0000000..dec9930
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0"
+    >
+    <path
+        android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z"
+        android:fillColor="#fff"/>
+    <path
+        android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z"
+        android:fillColor="#fff"/>
+</vector>
diff --git a/packages/SystemUI/res/layout/bindable_status_bar_icon.xml b/packages/SystemUI/res/layout/bindable_status_bar_icon.xml
new file mode 100644
index 0000000..ee4d05c
--- /dev/null
+++ b/packages/SystemUI/res/layout/bindable_status_bar_icon.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!-- Base layout that provides a single bindable icon_view id image view -->
+<com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:gravity="center_vertical"
+    >
+
+    <ImageView
+        android:id="@+id/icon_view"
+        android:layout_height="@dimen/status_bar_bindable_icon_size"
+        android:layout_width="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:padding="@dimen/status_bar_bindable_icon_padding"
+        android:scaleType="fitCenter"
+        />
+
+</com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView>
diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml
index cfb4017..55606aa 100644
--- a/packages/SystemUI/res/values-land/dimens.xml
+++ b/packages/SystemUI/res/values-land/dimens.xml
@@ -86,8 +86,8 @@
     <!-- Power Menu Lite -->
     <!-- These values are for small screen landscape. For larger landscape screens, they are
          overlaid -->
-    <dimen name="global_actions_button_size">72dp</dimen>
-    <dimen name="global_actions_button_padding">26dp</dimen>
+    <dimen name="global_actions_button_size">64dp</dimen>
+    <dimen name="global_actions_button_padding">20dp</dimen>
 
     <!-- scroll view the size of 2 channel rows -->
     <dimen name="notification_blocker_channel_list_height">128dp</dimen>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 798fc06b..9a4520c 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -151,6 +151,8 @@
     <dimen name="status_bar_icon_size_sp">@*android:dimen/status_bar_icon_size_sp</dimen>
     <!-- Original dp height of notification icons in the status bar -->
     <dimen name="status_bar_icon_size">@*android:dimen/status_bar_icon_size</dimen>
+    <dimen name="status_bar_bindable_icon_size">20sp</dimen>
+    <dimen name="status_bar_bindable_icon_padding">2sp</dimen>
 
     <!-- Default horizontal drawable padding for status bar icons. -->
     <dimen name="status_bar_horizontal_padding">2.5sp</dimen>
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt
index adbb37c..4184ef7 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt
@@ -31,6 +31,7 @@
         is PackageChangeModel.UpdateStarted -> "started updating"
         is PackageChangeModel.UpdateFinished -> "finished updating"
         is PackageChangeModel.Changed -> "changed"
+        is PackageChangeModel.Empty -> throw IllegalStateException("Unexpected empty value: $model")
     }
 
 /** A debug logger for [PackageChangeRepository]. */
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt b/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt
index 853eff7..3ae87c3 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt
@@ -23,6 +23,14 @@
     val packageName: String
     val packageUid: Int
 
+    /** Empty change, provided for convenience when a sensible default value is needed. */
+    data object Empty : PackageChangeModel {
+        override val packageName: String
+            get() = ""
+        override val packageUid: Int
+            get() = 0
+    }
+
     /**
      * An existing application package was uninstalled.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index 5bb2782..f37d9f8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -99,34 +99,4 @@
             context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) +
                 Utils.getStatusBarHeaderHeightKeyguard(context)
         }
-
-    fun getLargeClockTopMargin(context: Context): Int {
-        var largeClockTopMargin =
-            context.resources.getDimensionPixelSize(R.dimen.status_bar_height) +
-                context.resources.getDimensionPixelSize(
-                    com.android.systemui.customization.R.dimen.small_clock_padding_top
-                ) +
-                context.resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset)
-        largeClockTopMargin += getDimen(context, DATE_WEATHER_VIEW_HEIGHT)
-        largeClockTopMargin += getDimen(context, ENHANCED_SMARTSPACE_HEIGHT)
-        if (!useLargeClock) {
-            largeClockTopMargin -=
-                context.resources.getDimensionPixelSize(
-                    com.android.systemui.customization.R.dimen.small_clock_height
-                )
-        }
-
-        return largeClockTopMargin
-    }
-
-    private fun getDimen(context: Context, name: String): Int {
-        val res = context.packageManager.getResourcesForApplication(context.packageName)
-        val id = res.getIdentifier(name, "dimen", context.packageName)
-        return res.getDimensionPixelSize(id)
-    }
-
-    companion object {
-        private const val DATE_WEATHER_VIEW_HEIGHT = "date_weather_view_height"
-        private const val ENHANCED_SMARTSPACE_HEIGHT = "enhanced_smartspace_height"
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
index 36bbe4e..d792889 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
@@ -16,9 +16,13 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import android.content.res.Resources
+import com.android.keyguard.KeyguardClockSwitch
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
+import com.android.systemui.res.R
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
@@ -31,12 +35,28 @@
 class LockscreenContentViewModel
 @Inject
 constructor(
+    clockInteractor: KeyguardClockInteractor,
     private val interactor: KeyguardBlueprintInteractor,
     private val authController: AuthController,
     val longPress: KeyguardLongPressViewModel,
 ) {
+    private val clockSize = clockInteractor.clockSize
+
     val isUdfpsVisible: Boolean
         get() = authController.isUdfpsSupported
+    val isLargeClockVisible: Boolean
+        get() = clockSize.value == KeyguardClockSwitch.LARGE
+    val areNotificationsVisible: Boolean
+        get() = !isLargeClockVisible
+
+    fun getSmartSpacePaddingTop(resources: Resources): Int {
+        return if (isLargeClockVisible) {
+            resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) +
+                resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin)
+        } else {
+            0
+        }
+    }
 
     fun blueprintId(scope: CoroutineScope): StateFlow<String> {
         return interactor.blueprint
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
index 6e7e099..bcd09bd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
@@ -18,23 +18,21 @@
 
 import android.Manifest.permission.BIND_QUICK_SETTINGS_TILE
 import android.annotation.WorkerThread
-import android.content.BroadcastReceiver
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
-import android.content.IntentFilter
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.ResolveInfoFlags
 import android.os.UserHandle
 import android.service.quicksettings.TileService
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.common.data.repository.PackageChangeRepository
+import com.android.systemui.common.data.shared.model.PackageChangeModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.util.kotlin.isComponentActuallyEnabled
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOn
@@ -52,6 +50,7 @@
 constructor(
     @Application private val applicationContext: Context,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val packageChangeRepository: PackageChangeRepository
 ) : InstalledTilesComponentRepository {
 
     override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> {
@@ -70,24 +69,9 @@
                     )
                     .packageManager
             }
-        return conflatedCallbackFlow {
-                val receiver =
-                    object : BroadcastReceiver() {
-                        override fun onReceive(context: Context?, intent: Intent?) {
-                            trySend(Unit)
-                        }
-                    }
-                applicationContext.registerReceiverAsUser(
-                    receiver,
-                    UserHandle.of(userId),
-                    INTENT_FILTER,
-                    /* broadcastPermission = */ null,
-                    /* scheduler = */ null
-                )
-
-                awaitClose { applicationContext.unregisterReceiver(receiver) }
-            }
-            .onStart { emit(Unit) }
+        return packageChangeRepository
+            .packageChanged(UserHandle.of(userId))
+            .onStart { emit(PackageChangeModel.Empty) }
             .map { reloadComponents(userId, packageManager) }
             .distinctUntilChanged()
             .flowOn(backgroundDispatcher)
@@ -104,14 +88,6 @@
     }
 
     companion object {
-        private val INTENT_FILTER =
-            IntentFilter().apply {
-                addAction(Intent.ACTION_PACKAGE_ADDED)
-                addAction(Intent.ACTION_PACKAGE_CHANGED)
-                addAction(Intent.ACTION_PACKAGE_REMOVED)
-                addAction(Intent.ACTION_PACKAGE_REPLACED)
-                addDataScheme("package")
-            }
         private val INTENT = Intent(TileService.ACTION_QS_TILE)
         private val FLAGS =
             ResolveInfoFlags.of(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationVisibilityProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationVisibilityProviderImpl.kt
index ec10aaf..88e94e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationVisibilityProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationVisibilityProviderImpl.kt
@@ -22,12 +22,17 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.logging.NotificationLogger
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import javax.inject.Inject
 
 /** pipeline-agnostic implementation for getting [NotificationVisibility]. */
 @SysUISingleton
-class NotificationVisibilityProviderImpl @Inject constructor(
+class NotificationVisibilityProviderImpl
+@Inject
+constructor(
+    private val activeNotificationsInteractor: ActiveNotificationsInteractor,
     private val notifDataStore: NotifLiveDataStore,
     private val notifCollection: CommonNotifCollection
 ) : NotificationVisibilityProvider {
@@ -47,5 +52,10 @@
     override fun getLocation(key: String): NotificationVisibility.NotificationLocation =
         NotificationLogger.getNotificationLocation(notifCollection.getEntry(key))
 
-    private fun getCount() = notifDataStore.activeNotifCount.value
+    private fun getCount() =
+        if (NotificationsLiveDataStoreRefactor.isEnabled) {
+            activeNotificationsInteractor.allNotificationsCountValue
+        } else {
+            notifDataStore.activeNotifCount.value
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
index ca6344d..5180bab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
@@ -61,6 +61,15 @@
     val allRepresentativeNotifications: Flow<Map<String, ActiveNotificationModel>> =
         repository.activeNotifications.map { store -> store.individuals }
 
+    /** Size of the flattened list of Notifications actively presented in the stack. */
+    val allNotificationsCount: Flow<Int> =
+        repository.activeNotifications.map { store -> store.individuals.size }
+
+    /**
+     * The same as [allNotificationsCount], but without flows, for easy access in synchronous code.
+     */
+    val allNotificationsCountValue: Int = repository.activeNotifications.value.individuals.size
+
     /** Are any notifications being actively presented in the notification stack? */
     val areAnyNotificationsPresent: Flow<Boolean> =
         repository.activeNotifications
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTracker.kt
new file mode 100644
index 0000000..a1fb983
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTracker.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack
+
+import com.android.internal.util.LatencyTracker
+import com.android.internal.util.LatencyTracker.ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE
+import com.android.internal.util.LatencyTracker.ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import javax.inject.Inject
+
+/**
+ * Tracks latencies related to temporary hiding notifications while measuring
+ * them, which is an optimization to show some content as early as possible
+ * and perform notifications measurement later.
+ * See [HideNotificationsInteractor].
+ */
+class DisplaySwitchNotificationsHiderTracker @Inject constructor(
+    private val notificationsInteractor: ShadeInteractor,
+    private val latencyTracker: LatencyTracker
+) {
+
+    suspend fun trackNotificationHideTime(shouldHideNotifications: Flow<Boolean>) {
+        shouldHideNotifications
+            .collect { shouldHide ->
+                if (shouldHide) {
+                    latencyTracker.onActionStart(ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE)
+                } else {
+                    latencyTracker.onActionEnd(ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE)
+                }
+            }
+    }
+
+    suspend fun trackNotificationHideTimeWhenVisible(shouldHideNotifications: Flow<Boolean>) {
+        combine(shouldHideNotifications, notificationsInteractor.isAnyExpanded)
+            { hidden, shadeExpanded -> hidden && shadeExpanded }
+            .distinctUntilChanged()
+            .collect { hiddenButShouldBeVisible ->
+                if (hiddenButShouldBeVisible) {
+                    latencyTracker.onActionStart(
+                            ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN)
+                } else {
+                    latencyTracker.onActionEnd(
+                            ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN)
+                }
+            }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
index 910b40f..c2bc9ba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
@@ -16,22 +16,36 @@
 package com.android.systemui.statusbar.notification.stack.ui.viewbinder
 
 import androidx.core.view.doOnDetach
+import com.android.systemui.statusbar.notification.stack.DisplaySwitchNotificationsHiderTracker
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.launch
 
 /**
  * Binds a [NotificationStackScrollLayoutController] to its [view model][NotificationListViewModel].
  */
 object HideNotificationsBinder {
-    suspend fun bindHideList(
+    fun CoroutineScope.bindHideList(
         viewController: NotificationStackScrollLayoutController,
-        viewModel: NotificationListViewModel
+        viewModel: NotificationListViewModel,
+        hiderTracker: DisplaySwitchNotificationsHiderTracker
     ) {
         viewController.view.doOnDetach { viewController.bindHideState(shouldHide = false) }
 
-        viewModel.hideListViewModel.shouldHideListForPerformance.collect { shouldHide ->
-            viewController.bindHideState(shouldHide)
+        val hideListFlow = viewModel.hideListViewModel.shouldHideListForPerformance
+            .shareIn(this, started = Lazily)
+
+        launch {
+            hideListFlow.collect { shouldHide ->
+                viewController.bindHideState(shouldHide)
+            }
         }
+
+        launch { hiderTracker.trackNotificationHideTime(hideListFlow) }
+        launch { hiderTracker.trackNotificationHideTimeWhenVisible(hideListFlow) }
     }
 
     private fun NotificationStackScrollLayoutController.bindHideState(shouldHide: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index da260cb..44a7e7e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder
+import com.android.systemui.statusbar.notification.stack.DisplaySwitchNotificationsHiderTracker
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger
@@ -53,6 +54,7 @@
 @Inject
 constructor(
     @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val hiderTracker: DisplaySwitchNotificationsHiderTracker,
     private val configuration: ConfigurationState,
     private val falsingManager: FalsingManager,
     private val iconAreaController: NotificationIconAreaController,
@@ -74,7 +76,7 @@
         view.repeatWhenAttached {
             lifecycleScope.launch {
                 launch { bindShelf(shelf) }
-                launch { bindHideList(viewController, viewModel) }
+                bindHideList(viewController, viewModel, hiderTracker)
 
                 if (FooterViewRefactor.isEnabled) {
                     launch { bindFooter(view) }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/OemSatelliteInputLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/OemSatelliteInputLog.kt
new file mode 100644
index 0000000..252945f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/OemSatelliteInputLog.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.dagger
+
+import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import javax.inject.Qualifier
+
+/** Detailed [DeviceBasedSatelliteRepository] logs */
+@Qualifier
+@MustBeDocumented
+@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
+annotation class OemSatelliteInputLog
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index e309c32..2b90e64 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -265,6 +265,13 @@
             return factory.create("VerboseMobileViewLog", 100)
         }
 
+        @Provides
+        @SysUISingleton
+        @OemSatelliteInputLog
+        fun provideOemSatelliteInputLog(factory: LogBufferFactory): LogBuffer {
+            return factory.create("DeviceBasedSatelliteInputLog", 32)
+        }
+
         const val FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON =
             "FirstMobileSubShowingNetworkTypeIcon"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt
index e3c3139..8400fb0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt
@@ -18,6 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon
+import com.android.systemui.statusbar.pipeline.satellite.ui.DeviceBasedSatelliteBindableIcon
 import javax.inject.Inject
 
 /**
@@ -38,11 +39,12 @@
 class BindableIconsRegistryImpl
 @Inject
 constructor(
-/** Bindables go here */
+    /** Bindables go here */
+    oemSatellite: DeviceBasedSatelliteBindableIcon
 ) : BindableIconsRegistry {
     /**
      * Adding the injected bindables to this list will get them registered with
      * StatusBarIconController
      */
-    override val bindableIcons: List<BindableIcon> = listOf()
+    override val bindableIcons: List<BindableIcon> = listOf(oemSatellite)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index de46a5e..5e6e36d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -19,11 +19,17 @@
 import android.os.OutcomeReceiver
 import android.telephony.satellite.NtnSignalStrengthCallback
 import android.telephony.satellite.SatelliteManager
+import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS
 import android.telephony.satellite.SatelliteModemStateCallback
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.MessageInitializer
+import com.android.systemui.log.core.MessagePrinter
+import com.android.systemui.statusbar.pipeline.dagger.OemSatelliteInputLog
 import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported
@@ -123,6 +129,7 @@
     satelliteManagerOpt: Optional<SatelliteManager>,
     @Background private val bgDispatcher: CoroutineDispatcher,
     @Application private val scope: CoroutineScope,
+    @OemSatelliteInputLog private val logBuffer: LogBuffer,
     private val systemClock: SystemClock,
 ) : DeviceBasedSatelliteRepository {
 
@@ -145,6 +152,11 @@
                 ensureMinUptime(systemClock, MIN_UPTIME)
                 satelliteSupport.value = satelliteManager.checkSatelliteSupported()
 
+                logBuffer.i(
+                    { str1 = satelliteSupport.value.toString() },
+                    { "Checked for system support. support=$str1" },
+                )
+
                 // We only need to check location availability if this mode is supported
                 if (satelliteSupport.value is Supported) {
                     isSatelliteAllowedForCurrentLocation.subscriptionCount
@@ -159,6 +171,9 @@
                                  * connection might cause more frequent checks.
                                  */
                                 while (true) {
+                                    logBuffer.i {
+                                        "requestIsSatelliteCommunicationAllowedForCurrentLocation"
+                                    }
                                     checkIsSatelliteAllowed()
                                     delay(POLLING_INTERVAL_MS)
                                 }
@@ -167,6 +182,8 @@
                 }
             }
         } else {
+            logBuffer.i { "Satellite manager is null" }
+
             satelliteSupport.value = NotSupported
         }
     }
@@ -181,12 +198,21 @@
     private fun connectionStateFlow(sm: SupportedSatelliteManager): Flow<SatelliteConnectionState> =
         conflatedCallbackFlow {
                 val cb = SatelliteModemStateCallback { state ->
+                    logBuffer.i({ int1 = state }) { "onSatelliteModemStateChanged: state=$int1" }
                     trySend(SatelliteConnectionState.fromModemState(state))
                 }
 
-                sm.registerForSatelliteModemStateChanged(bgDispatcher.asExecutor(), cb)
+                var registered = false
 
-                awaitClose { sm.unregisterForSatelliteModemStateChanged(cb) }
+                try {
+                    val res =
+                        sm.registerForSatelliteModemStateChanged(bgDispatcher.asExecutor(), cb)
+                    registered = res == SATELLITE_RESULT_SUCCESS
+                } catch (e: Exception) {
+                    logBuffer.e("error registering for modem state", e)
+                }
+
+                awaitClose { if (registered) sm.unregisterForSatelliteModemStateChanged(cb) }
             }
             .flowOn(bgDispatcher)
 
@@ -197,12 +223,21 @@
     private fun signalStrengthFlow(sm: SupportedSatelliteManager) =
         conflatedCallbackFlow {
                 val cb = NtnSignalStrengthCallback { signalStrength ->
+                    logBuffer.i({ int1 = signalStrength.level }) {
+                        "onNtnSignalStrengthChanged: level=$int1"
+                    }
                     trySend(signalStrength.level)
                 }
 
-                sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb)
+                var registered = false
+                try {
+                    sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb)
+                    registered = true
+                } catch (e: Exception) {
+                    logBuffer.e("error registering for signal strength", e)
+                }
 
-                awaitClose { sm.unregisterForNtnSignalStrengthChanged(cb) }
+                awaitClose { if (registered) sm.unregisterForNtnSignalStrengthChanged(cb) }
             }
             .flowOn(bgDispatcher)
 
@@ -213,11 +248,15 @@
                 bgDispatcher.asExecutor(),
                 object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> {
                     override fun onError(e: SatelliteManager.SatelliteException) {
-                        android.util.Log.e(TAG, "Found exception when checking for satellite: ", e)
+                        logBuffer.e(
+                            "Found exception when checking availability",
+                            e,
+                        )
                         isSatelliteAllowedForCurrentLocation.value = false
                     }
 
                     override fun onResult(allowed: Boolean) {
+                        logBuffer.i { allowed.toString() }
                         isSatelliteAllowedForCurrentLocation.value = allowed
                     }
                 }
@@ -239,6 +278,12 @@
                     }
 
                     override fun onError(error: SatelliteManager.SatelliteException) {
+                        logBuffer.e(
+                            "Exception when checking for satellite support. " +
+                                "Assuming it is not supported for this device.",
+                            error,
+                        )
+
                         // Assume that an error means it's not supported
                         continuation.resume(NotSupported)
                     }
@@ -264,5 +309,19 @@
                 delay(timeTilMinUptime)
             }
         }
+
+        /** A couple of convenience logging methods rather than a whole class */
+        private fun LogBuffer.i(
+            initializer: MessageInitializer = {},
+            printer: MessagePrinter,
+        ) = this.log(TAG, LogLevel.INFO, initializer, printer)
+
+        private fun LogBuffer.e(message: String, exception: Throwable? = null) =
+            this.log(
+                tag = TAG,
+                level = LogLevel.ERROR,
+                message = message,
+                exception = exception,
+            )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt
new file mode 100644
index 0000000..f5d0f6b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.ui
+
+import android.content.Context
+import com.android.internal.telephony.flags.Flags.oemEnabledSatelliteFlag
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon
+import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator
+import com.android.systemui.statusbar.pipeline.satellite.ui.binder.DeviceBasedSatelliteIconBinder
+import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel
+import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView
+import javax.inject.Inject
+
+@SysUISingleton
+class DeviceBasedSatelliteBindableIcon
+@Inject
+constructor(
+    context: Context,
+    viewModel: DeviceBasedSatelliteViewModel,
+) : BindableIcon {
+    override val slot: String =
+        context.getString(com.android.internal.R.string.status_bar_oem_satellite)
+
+    override val initializer = ModernStatusBarViewCreator { context ->
+        SingleBindableStatusBarIconView.createView(context).also { view ->
+            view.initView(slot) { DeviceBasedSatelliteIconBinder.bind(view, viewModel) }
+        }
+    }
+
+    override val shouldBindIcon: Boolean = oemEnabledSatelliteFlag()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt
new file mode 100644
index 0000000..59ac5f2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.ui.binder
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.common.ui.binder.IconViewBinder
+import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
+import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView
+import kotlinx.coroutines.launch
+
+object DeviceBasedSatelliteIconBinder {
+    fun bind(
+        view: SingleBindableStatusBarIconView,
+        viewModel: DeviceBasedSatelliteViewModel,
+    ): ModernStatusBarViewBinding {
+        return SingleBindableStatusBarIconView.withDefaultBinding(
+            view = view,
+            shouldBeVisible = { viewModel.icon.value != null }
+        ) {
+            lifecycleScope.launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    viewModel.icon.collect { newIcon ->
+                        if (newIcon == null) {
+                            view.iconView.setImageDrawable(null)
+                        } else {
+                            IconViewBinder.bind(newIcon, view.iconView)
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt
new file mode 100644
index 0000000..6938d66
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.ui.model
+
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+
+/**
+ * Define the [Icon] that relates to a given satellite connection state + level. Note that for now
+ * We don't need any data class box, so we can just use a simple mapping function.
+ */
+object SatelliteIconModel {
+    fun fromConnectionState(
+        connectionState: SatelliteConnectionState,
+        signalStrength: Int,
+    ): Icon? =
+        when (connectionState) {
+            // TODO(b/316635648): check if this should be null
+            SatelliteConnectionState.Unknown,
+            SatelliteConnectionState.Off,
+            SatelliteConnectionState.On ->
+                Icon.Resource(
+                    res = R.drawable.ic_satellite_not_connected,
+                    contentDescription = null,
+                )
+            SatelliteConnectionState.Connected -> fromSignalStrength(signalStrength)
+        }
+
+    private fun fromSignalStrength(
+        signalStrength: Int,
+    ): Icon? =
+        // TODO(b/316634365): these need content descriptions
+        when (signalStrength) {
+            // No signal
+            0 -> Icon.Resource(res = R.drawable.ic_satellite_connected_0, contentDescription = null)
+
+            // Poor -> Moderate
+            1,
+            2 -> Icon.Resource(res = R.drawable.ic_satellite_connected_1, contentDescription = null)
+
+            // Good -> Great
+            3,
+            4 -> Icon.Resource(res = R.drawable.ic_satellite_connected_2, contentDescription = null)
+            else -> null
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
new file mode 100644
index 0000000..0051161
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel
+
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor
+import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * View-Model for the device-based satellite icon. This icon will only show in the status bar if
+ * satellite is available AND all other service states are considered OOS.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class DeviceBasedSatelliteViewModel
+@Inject
+constructor(
+    interactor: DeviceBasedSatelliteInteractor,
+    @Application scope: CoroutineScope,
+) {
+    private val shouldShowIcon: StateFlow<Boolean> =
+        interactor.areAllConnectionsOutOfService
+            .flatMapLatest { allOos ->
+                if (!allOos) {
+                    flowOf(false)
+                } else {
+                    interactor.isSatelliteAllowed
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+    val icon: StateFlow<Icon?> =
+        combine(
+                shouldShowIcon,
+                interactor.connectionState,
+                interactor.signalStrength,
+            ) { shouldShow, state, signalStrength ->
+                if (shouldShow) {
+                    SatelliteIconModel.fromConnectionState(state, signalStrength)
+                } else {
+                    null
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
index 3b87bed..25a2c9d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt
@@ -103,7 +103,7 @@
      *
      * Creates a dot view, and uses [bindingCreator] to get and set the binding.
      */
-    fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) {
+    open fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) {
         // The dot view requires [slot] to be set, and the [binding] may require an instantiated dot
         // view. So, this is the required order.
         this.slot = slot
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt
new file mode 100644
index 0000000..c663c37
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.shared.ui.view
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.ImageView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+
+/** Simple single-icon view that is bound to bindable_status_bar_icon.xml */
+class SingleBindableStatusBarIconView(
+    context: Context,
+    attrs: AttributeSet?,
+) : ModernStatusBarView(context, attrs) {
+
+    internal lateinit var iconView: ImageView
+    internal lateinit var dotView: StatusBarIconView
+
+    override fun toString(): String {
+        return "SingleBindableStatusBarIcon(" +
+            "slot='$slot', " +
+            "isCollecting=${binding.isCollecting()}, " +
+            "visibleState=${StatusBarIconView.getVisibleStateString(visibleState)}); " +
+            "viewString=${super.toString()}"
+    }
+
+    override fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) {
+        super.initView(slot, bindingCreator)
+
+        iconView = requireViewById(R.id.icon_view)
+        dotView = requireViewById(R.id.status_bar_dot)
+    }
+
+    companion object {
+        fun createView(
+            context: Context,
+        ): SingleBindableStatusBarIconView {
+            return LayoutInflater.from(context).inflate(R.layout.bindable_status_bar_icon, null)
+                as SingleBindableStatusBarIconView
+        }
+
+        /**
+         * Using a given binding [block], create the necessary scaffolding to handle the general
+         * case of a single status bar icon. This includes eliding into a dot view when there is not
+         * enough space, and handling tint.
+         *
+         * [block] should be a simple [launch] call that handles updating the single icon view with
+         * its new view. Currently there is no simple way to e.g., extend to handle multiple tints
+         * for dual-layered icons, and any more complex logic should probably find a way to return
+         * its own version of [ModernStatusBarViewBinding].
+         */
+        fun withDefaultBinding(
+            view: SingleBindableStatusBarIconView,
+            shouldBeVisible: () -> Boolean,
+            block: suspend LifecycleOwner.(View) -> Unit
+        ): SingleBindableStatusBarIconViewBinding {
+            @StatusBarIconView.VisibleState
+            val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN)
+
+            val iconTint: MutableStateFlow<Int> = MutableStateFlow(Color.WHITE)
+            val decorTint: MutableStateFlow<Int> = MutableStateFlow(Color.WHITE)
+
+            var isCollecting: Boolean = false
+
+            view.repeatWhenAttached {
+                // Child binding
+                block(view)
+
+                lifecycleScope.launch {
+                    repeatOnLifecycle(Lifecycle.State.STARTED) {
+                        // isVisible controls the visibility state of the outer group, and thus it
+                        // needs
+                        // to run in the CREATED lifecycle so it can continue to watch while
+                        // invisible
+                        // See (b/291031862) for details
+                        launch {
+                            visibilityState.collect { visibilityState ->
+                                // for b/296864006, we can not hide all the child views if
+                                // visibilityState is STATE_HIDDEN. Because hiding all child views
+                                // would cause the
+                                // getWidth() of this view return 0, and that would cause the
+                                // translation
+                                // calculation fails in StatusIconContainer. Therefore, like class
+                                // MobileIconBinder, instead of set the child views visibility to
+                                // View.GONE,
+                                // we set their visibility to View.INVISIBLE to make them invisible
+                                // but
+                                // keep the width.
+                                ModernStatusBarViewVisibilityHelper.setVisibilityState(
+                                    visibilityState,
+                                    view.iconView,
+                                    view.dotView,
+                                )
+                            }
+                        }
+
+                        launch {
+                            iconTint.collect { tint ->
+                                val tintList = ColorStateList.valueOf(tint)
+                                view.iconView.imageTintList = tintList
+                                view.dotView.setDecorColor(tint)
+                            }
+                        }
+
+                        launch {
+                            decorTint.collect { decorTint -> view.dotView.setDecorColor(decorTint) }
+                        }
+
+                        try {
+                            awaitCancellation()
+                        } finally {
+                            isCollecting = false
+                        }
+                    }
+                }
+            }
+
+            return object : SingleBindableStatusBarIconViewBinding {
+                override val decorTint: Int
+                    get() = decorTint.value
+
+                override val iconTint: Int
+                    get() = iconTint.value
+
+                override fun getShouldIconBeVisible(): Boolean {
+                    return shouldBeVisible()
+                }
+
+                override fun onVisibilityStateChanged(state: Int) {
+                    visibilityState.value = state
+                }
+
+                override fun onIconTintChanged(newTint: Int, contrastTint: Int) {
+                    iconTint.value = newTint
+                }
+
+                override fun onDecorTintChanged(newTint: Int) {
+                    decorTint.value = newTint
+                }
+
+                override fun isCollecting(): Boolean {
+                    return isCollecting
+                }
+            }
+        }
+    }
+}
+
+@VisibleForTesting
+interface SingleBindableStatusBarIconViewBinding : ModernStatusBarViewBinding {
+    val iconTint: Int
+    val decorTint: Int
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
index b3b10eb..9b641f0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
@@ -50,6 +50,18 @@
         DaggerActiveNotificationsInteractorTest_TestComponent.factory().create(test = this)
 
     @Test
+    fun testAllNotificationsCount() =
+        testComponent.runTest {
+            val count by collectLastValue(underTest.allNotificationsCount)
+
+            activeNotificationListRepository.setActiveNotifs(5)
+            runCurrent()
+
+            assertThat(count).isEqualTo(5)
+            assertThat(underTest.allNotificationsCountValue).isEqualTo(5)
+        }
+
+    @Test
     fun testAreAnyNotificationsPresent_isTrue() =
         testComponent.runTest {
             val areAnyNotificationsPresent by collectLastValue(underTest.areAnyNotificationsPresent)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTrackerTest.kt
new file mode 100644
index 0000000..1dfcb38
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTrackerTest.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.stack
+
+import androidx.test.filters.SmallTest
+import com.android.internal.util.LatencyTracker
+import com.android.internal.util.LatencyTracker.ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE
+import com.android.internal.util.LatencyTracker.ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+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.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class DisplaySwitchNotificationsHiderTrackerTest : SysuiTestCase() {
+
+    private val testScope = TestScope()
+    private val shadeInteractor = mock<ShadeInteractor>()
+    private val latencyTracker = mock<LatencyTracker>()
+
+    private val shouldHideFlow = MutableStateFlow(false)
+    private val shadeExpandedFlow = MutableStateFlow(false)
+
+    private val tracker = DisplaySwitchNotificationsHiderTracker(shadeInteractor, latencyTracker)
+
+    @Before
+    fun setup() {
+        whenever(shadeInteractor.isAnyExpanded).thenReturn(shadeExpandedFlow)
+    }
+
+    @Test
+    fun notificationsBecomeHidden_tracksHideActionStart() = testScope.runTest {
+        startTracking()
+
+        shouldHideFlow.value = true
+        runCurrent()
+
+        verify(latencyTracker).onActionStart(HIDE_NOTIFICATIONS_ACTION)
+    }
+
+    @Test
+    fun notificationsBecomeVisibleAfterHidden_tracksHideActionEnd() = testScope.runTest {
+        startTracking()
+
+        shouldHideFlow.value = true
+        runCurrent()
+        clearInvocations(latencyTracker)
+        shouldHideFlow.value = false
+        runCurrent()
+
+        verify(latencyTracker).onActionEnd(HIDE_NOTIFICATIONS_ACTION)
+    }
+
+    @Test
+    fun notificationsBecomeHiddenWhenShadeIsClosed_doesNotTrackHideWhenVisibleActionStart() =
+            testScope.runTest {
+                shouldHideFlow.value = false
+                shadeExpandedFlow.value = false
+                startTracking()
+
+                shouldHideFlow.value = true
+                runCurrent()
+
+                verify(latencyTracker, never())
+                        .onActionStart(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION)
+            }
+
+    @Test
+    fun notificationsBecomeHiddenWhenShadeIsOpen_tracksHideWhenVisibleActionStart() = testScope.runTest {
+        shouldHideFlow.value = false
+        shadeExpandedFlow.value = false
+        startTracking()
+
+        shouldHideFlow.value = true
+        shadeExpandedFlow.value = true
+        runCurrent()
+
+        verify(latencyTracker).onActionStart(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION)
+    }
+
+    @Test
+    fun shadeBecomesOpenWhenNotificationsHidden_tracksHideWhenVisibleActionStart() =
+            testScope.runTest {
+            shouldHideFlow.value = true
+            shadeExpandedFlow.value = false
+            startTracking()
+
+            shadeExpandedFlow.value = true
+            runCurrent()
+
+            verify(latencyTracker).onActionStart(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION)
+        }
+
+    @Test
+    fun notificationsBecomeVisibleWhenShadeIsOpen_tracksHideWhenVisibleActionEnd() = testScope.runTest {
+        shouldHideFlow.value = false
+        shadeExpandedFlow.value = false
+        startTracking()
+        shouldHideFlow.value = true
+        shadeExpandedFlow.value = true
+        runCurrent()
+        clearInvocations(latencyTracker)
+
+        shouldHideFlow.value = false
+        runCurrent()
+
+        verify(latencyTracker).onActionEnd(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION)
+    }
+
+    @Test
+    fun shadeBecomesClosedWhenNotificationsHidden_tracksHideWhenVisibleActionEnd() = testScope.runTest {
+        shouldHideFlow.value = false
+        shadeExpandedFlow.value = false
+        startTracking()
+        shouldHideFlow.value = true
+        shadeExpandedFlow.value = true
+        runCurrent()
+        clearInvocations(latencyTracker)
+
+        shadeExpandedFlow.value = false
+        runCurrent()
+
+        verify(latencyTracker).onActionEnd(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION)
+    }
+
+    private fun TestScope.startTracking() {
+        backgroundScope.launch { tracker.trackNotificationHideTime(shouldHideFlow) }
+        backgroundScope.launch { tracker.trackNotificationHideTimeWhenVisible(shouldHideFlow) }
+        runCurrent()
+        clearInvocations(latencyTracker)
+    }
+
+    private companion object {
+        const val HIDE_NOTIFICATIONS_ACTION = ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE
+        const val HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION =
+                ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
index 02e6fd5..7d91e8b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -35,6 +35,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.log.core.FakeLogBuffer
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.MIN_UPTIME
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS
 import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
@@ -87,6 +88,7 @@
                     Optional.empty(),
                     dispatcher,
                     testScope.backgroundScope,
+                    FakeLogBuffer.Factory.create(),
                     systemClock,
                 )
 
@@ -100,6 +102,22 @@
         }
 
     @Test
+    fun satelliteManagerThrows_doesNotCrash() =
+        testScope.runTest {
+            setupDefaultRepo()
+
+            whenever(satelliteManager.registerForNtnSignalStrengthChanged(any(), any()))
+                .thenThrow(SatelliteException(13))
+
+            val conn by collectLastValue(underTest.connectionState)
+            val strength by collectLastValue(underTest.signalStrength)
+
+            // Flows have not emitted, we haven't crashed
+            assertThat(conn).isNull()
+            assertThat(strength).isNull()
+        }
+
+    @Test
     fun connectionState_mapsFromSatelliteModemState() =
         testScope.runTest {
             setupDefaultRepo()
@@ -380,6 +398,7 @@
                 if (satMan != null) Optional.of(satMan) else Optional.empty(),
                 dispatcher,
                 testScope.backgroundScope,
+                FakeLogBuffer.Factory.create(),
                 systemClock,
             )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
new file mode 100644
index 0000000..21c038a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class DeviceBasedSatelliteViewModelTest : SysuiTestCase() {
+    private lateinit var underTest: DeviceBasedSatelliteViewModel
+    private lateinit var interactor: DeviceBasedSatelliteInteractor
+
+    private val repo = FakeDeviceBasedSatelliteRepository()
+    private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
+
+    private val testScope = TestScope()
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        interactor =
+            DeviceBasedSatelliteInteractor(
+                repo,
+                mobileIconsInteractor,
+                testScope.backgroundScope,
+            )
+
+        underTest =
+            DeviceBasedSatelliteViewModel(
+                interactor,
+                testScope.backgroundScope,
+            )
+    }
+
+    @Test
+    fun icon_nullWhenShouldNotShow_satelliteNotAllowed() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.icon)
+
+            // GIVEN satellite is not allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = false
+
+            // GIVEN all icons are OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+
+            // THEN icon is null because we should not be showing it
+            assertThat(latest).isNull()
+        }
+
+    @Test
+    fun icon_nullWhenShouldNotShow_notAllOos() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.icon)
+
+            // GIVEN satellite is allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+
+            // GIVEN all icons are not OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = true
+
+            // THEN icon is null because we have service
+            assertThat(latest).isNull()
+        }
+
+    @Test
+    fun icon_satelliteIsOff() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.icon)
+
+            // GIVEN satellite is allowed
+            repo.isSatelliteAllowedForCurrentLocation.value = true
+
+            // GIVEN all icons are OOS
+            val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+            i1.isInService.value = false
+
+            // THEN icon is null because we have service
+            assertThat(latest).isInstanceOf(Icon::class.java)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt
new file mode 100644
index 0000000..ca9df57
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.shared.ui.view
+
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.StatusBarIconView
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Being a simple subclass of [ModernStatusBarView], use the same basic test cases to verify the
+ * root behavior, and add testing for the new [SingleBindableStatusBarIconView.withDefaultBinding]
+ * method.
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class SingleBindableStatusBarIconViewTest : SysuiTestCase() {
+    private lateinit var binding: SingleBindableStatusBarIconViewBinding
+
+    // Visibility is outsourced to view-models. This simulates it
+    private var isVisible = true
+    private var visibilityFn: () -> Boolean = { isVisible }
+
+    @Test
+    fun initView_hasCorrectSlot() {
+        val view = createAndInitView()
+
+        assertThat(view.slot).isEqualTo(SLOT_NAME)
+    }
+
+    @Test
+    fun getVisibleState_icon_returnsIcon() {
+        val view = createAndInitView()
+
+        view.setVisibleState(StatusBarIconView.STATE_ICON, /* animate= */ false)
+
+        assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_ICON)
+    }
+
+    @Test
+    fun getVisibleState_dot_returnsDot() {
+        val view = createAndInitView()
+
+        view.setVisibleState(StatusBarIconView.STATE_DOT, /* animate= */ false)
+
+        assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_DOT)
+    }
+
+    @Test
+    fun getVisibleState_hidden_returnsHidden() {
+        val view = createAndInitView()
+
+        view.setVisibleState(StatusBarIconView.STATE_HIDDEN, /* animate= */ false)
+
+        assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_HIDDEN)
+    }
+
+    @Test
+    fun onDarkChanged_bindingReceivesIconAndDecorTint() {
+        val view = createAndInitView()
+
+        view.onDarkChangedWithContrast(arrayListOf(), 0x12345678, 0x12344321)
+
+        assertThat(binding.iconTint).isEqualTo(0x12345678)
+        assertThat(binding.decorTint).isEqualTo(0x12345678)
+    }
+
+    @Test
+    fun setStaticDrawableColor_bindingReceivesIconTint() {
+        val view = createAndInitView()
+
+        view.setStaticDrawableColor(0x12345678, 0x12344321)
+
+        assertThat(binding.iconTint).isEqualTo(0x12345678)
+    }
+
+    @Test
+    fun setDecorColor_bindingReceivesDecorColor() {
+        val view = createAndInitView()
+
+        view.setDecorColor(0x23456789)
+
+        assertThat(binding.decorTint).isEqualTo(0x23456789)
+    }
+
+    @Test
+    fun isIconVisible_usesBinding_true() {
+        val view = createAndInitView()
+
+        isVisible = true
+
+        assertThat(view.isIconVisible).isEqualTo(true)
+    }
+
+    @Test
+    fun isIconVisible_usesBinding_false() {
+        val view = createAndInitView()
+
+        isVisible = false
+
+        assertThat(view.isIconVisible).isEqualTo(false)
+    }
+
+    @Test
+    fun getDrawingRect_takesTranslationIntoAccount() {
+        val view = createAndInitView()
+
+        view.translationX = 50f
+        view.translationY = 60f
+
+        val drawingRect = Rect()
+        view.getDrawingRect(drawingRect)
+
+        assertThat(drawingRect.left).isEqualTo(view.left + 50)
+        assertThat(drawingRect.right).isEqualTo(view.right + 50)
+        assertThat(drawingRect.top).isEqualTo(view.top + 60)
+        assertThat(drawingRect.bottom).isEqualTo(view.bottom + 60)
+    }
+
+    private fun createAndInitView(): SingleBindableStatusBarIconView {
+        val view = SingleBindableStatusBarIconView.createView(context)
+        binding = SingleBindableStatusBarIconView.withDefaultBinding(view, visibilityFn) {}
+        view.initView(SLOT_NAME) { binding }
+        return view
+    }
+
+    companion object {
+        private const val SLOT_NAME = "test_slot"
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/android/view/accessibility/AccessibilityManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/view/accessibility/AccessibilityManagerKosmos.kt
index a11bf6a..d095f42 100644
--- a/packages/SystemUI/tests/utils/src/android/view/accessibility/AccessibilityManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/view/accessibility/AccessibilityManagerKosmos.kt
@@ -18,6 +18,11 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
 import com.android.systemui.util.mockito.mock
 
 var Kosmos.accessibilityManager by Fixture { mock<AccessibilityManager>() }
+
+var Kosmos.accessibilityManagerWrapper by Fixture {
+    AccessibilityManagerWrapper(accessibilityManager)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt
index eedc0b9..22dff0a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt
@@ -17,8 +17,11 @@
 package com.android.internal.logging
 
 import com.android.internal.logging.testing.FakeMetricsLogger
+import com.android.internal.util.LatencyTracker
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.util.mockito.mock
 
 val Kosmos.fakeMetricsLogger by Fixture { FakeMetricsLogger() }
 val Kosmos.metricsLogger by Fixture<MetricsLogger> { fakeMetricsLogger }
+val Kosmos.latencyTracker by Fixture { mock<LatencyTracker>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
new file mode 100644
index 0000000..19cd950
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import com.android.systemui.common.ui.data.repository.configurationRepository
+import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
+import com.android.systemui.keyguard.shared.model.KeyguardSection
+import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.keyguardBlueprintRepository by
+    Kosmos.Fixture {
+        KeyguardBlueprintRepository(
+            configurationRepository = configurationRepository,
+            blueprints = setOf(defaultBlueprint),
+        )
+    }
+
+private val defaultBlueprint =
+    object : KeyguardBlueprint {
+        override val id: String
+            get() = DEFAULT
+        override val sections: List<KeyguardSection>
+            get() = listOf()
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt
new file mode 100644
index 0000000..d9a3192
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.content.applicationContext
+import com.android.systemui.keyguard.data.repository.keyguardBlueprintRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.policy.splitShadeStateController
+
+val Kosmos.keyguardBlueprintInteractor by
+    Kosmos.Fixture {
+        KeyguardBlueprintInteractor(
+            keyguardBlueprintRepository = keyguardBlueprintRepository,
+            applicationScope = applicationCoroutineScope,
+            context = applicationContext,
+            splitShadeStateController = splitShadeStateController,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
new file mode 100644
index 0000000..638a6a3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.content.applicationContext
+import android.view.accessibility.accessibilityManagerWrapper
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.qs.uiEventLogger
+
+val Kosmos.keyguardLongPressInteractor by
+    Kosmos.Fixture {
+        KeyguardLongPressInteractor(
+            appContext = applicationContext,
+            scope = applicationCoroutineScope,
+            transitionInteractor = keyguardTransitionInteractor,
+            repository = keyguardRepository,
+            logger = uiEventLogger,
+            featureFlags = featureFlagsClassic,
+            broadcastDispatcher = broadcastDispatcher,
+            accessibilityManager = accessibilityManagerWrapper,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModelKosmos.kt
new file mode 100644
index 0000000..3c9846a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModelKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.domain.interactor.keyguardLongPressInteractor
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.keyguardLongPressViewModel by
+    Kosmos.Fixture {
+        KeyguardLongPressViewModel(
+            interactor = keyguardLongPressInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
new file mode 100644
index 0000000..96de4ba
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.biometrics.authController
+import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.lockscreenContentViewModel by
+    Kosmos.Fixture {
+        LockscreenContentViewModel(
+            clockInteractor = keyguardClockInteractor,
+            interactor = keyguardBlueprintInteractor,
+            authController = authController,
+            longPress = keyguardLongPressViewModel,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotficationsHiderTrackerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotficationsHiderTrackerKosmos.kt
new file mode 100644
index 0000000..b12f5af
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotficationsHiderTrackerKosmos.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack
+
+import com.android.internal.logging.latencyTracker
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+
+val Kosmos.displaySwitchNotificationsHiderTracker by Fixture {
+    DisplaySwitchNotificationsHiderTracker(shadeInteractor, latencyTracker)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
index c6498e4..748d04d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.notificationIconContainerShelfViewBinder
 import com.android.systemui.statusbar.notification.stack.ui.view.notificationStatsLogger
+import com.android.systemui.statusbar.notification.stack.displaySwitchNotificationsHiderTracker
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel
 import com.android.systemui.statusbar.phone.notificationIconAreaController
 import java.util.Optional
@@ -36,6 +37,7 @@
         falsingManager = falsingManager,
         iconAreaController = notificationIconAreaController,
         metricsLogger = metricsLogger,
+        hiderTracker = displaySwitchNotificationsHiderTracker,
         nicBinder = notificationIconContainerShelfViewBinder,
         loggerOptional = Optional.of(notificationStatsLogger),
     )
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index c8be6b5..f13f49a 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -23,6 +23,7 @@
 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
 import static android.companion.virtual.VirtualDeviceParams.NAVIGATION_POLICY_DEFAULT_ALLOWED;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
 import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS;
@@ -261,7 +262,9 @@
                 runningAppsChangedCallback,
                 params,
                 DisplayManagerGlobal.getInstance(),
-                Flags.virtualCamera() ? new VirtualCameraController() : null);
+                Flags.virtualCamera()
+                        ? new VirtualCameraController(params.getDevicePolicy(POLICY_TYPE_CAMERA))
+                        : null);
     }
 
     @VisibleForTesting
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
index 2f9b6a5..2d82b5e 100644
--- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,10 +16,13 @@
 
 package com.android.server.companion.virtual.camera;
 
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+
 import static com.android.server.companion.virtual.camera.VirtualCameraConversionUtil.getServiceCameraConfiguration;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.companion.virtual.VirtualDeviceParams.DevicePolicy;
 import android.companion.virtual.camera.VirtualCameraConfig;
 import android.companion.virtualcamera.IVirtualCameraService;
 import android.companion.virtualcamera.VirtualCameraConfiguration;
@@ -51,15 +54,21 @@
 
     @GuardedBy("mServiceLock")
     @Nullable private IVirtualCameraService mVirtualCameraService;
+    @DevicePolicy
+    private final int mCameraPolicy;
 
     @GuardedBy("mCameras")
     private final Map<IBinder, CameraDescriptor> mCameras = new ArrayMap<>();
 
-    public VirtualCameraController() {}
+    public VirtualCameraController(@DevicePolicy int cameraPolicy) {
+        this(/* virtualCameraService= */ null, cameraPolicy);
+    }
 
     @VisibleForTesting
-    VirtualCameraController(IVirtualCameraService virtualCameraService) {
+    VirtualCameraController(IVirtualCameraService virtualCameraService,
+            @DevicePolicy int cameraPolicy) {
         mVirtualCameraService = virtualCameraService;
+        mCameraPolicy = cameraPolicy;
     }
 
     /**
@@ -68,6 +77,8 @@
      * @param cameraConfig The {@link VirtualCameraConfig} sent by the client.
      */
     public void registerCamera(@NonNull VirtualCameraConfig cameraConfig) {
+        checkConfigByPolicy(cameraConfig);
+
         connectVirtualCameraServiceIfNeeded();
 
         try {
@@ -173,6 +184,29 @@
         }
     }
 
+    private void checkConfigByPolicy(VirtualCameraConfig config) {
+        if (mCameraPolicy == DEVICE_POLICY_DEFAULT) {
+            throw new IllegalArgumentException(
+                    "Cannot create virtual camera with DEVICE_POLICY_DEFAULT for "
+                            + "POLICY_TYPE_CAMERA");
+        } else if (isLensFacingAlreadyPresent(config.getLensFacing())) {
+            throw new IllegalArgumentException(
+                    "Only a single virtual camera can be created with lens facing "
+                            + config.getLensFacing());
+        }
+    }
+
+    private boolean isLensFacingAlreadyPresent(int lensFacing) {
+        synchronized (mCameras) {
+            for (CameraDescriptor cameraDescriptor : mCameras.values()) {
+                if (cameraDescriptor.mConfig.getLensFacing() == lensFacing) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     private void connectVirtualCameraServiceIfNeeded() {
         synchronized (mServiceLock) {
             // Try to connect to service if not connected already.
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
index f24c4cc..c4a84b0 100644
--- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
@@ -25,6 +25,7 @@
 import android.companion.virtualcamera.SupportedStreamConfiguration;
 import android.companion.virtualcamera.VirtualCameraConfiguration;
 import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
 import android.os.RemoteException;
 import android.view.Surface;
 
@@ -45,12 +46,12 @@
             getServiceCameraConfiguration(@NonNull VirtualCameraConfig cameraConfig)
                     throws RemoteException {
         VirtualCameraConfiguration serviceConfiguration = new VirtualCameraConfiguration();
-
         serviceConfiguration.supportedStreamConfigs =
                 cameraConfig.getStreamConfigs().stream()
                         .map(VirtualCameraConversionUtil::convertSupportedStreamConfiguration)
                         .toArray(SupportedStreamConfiguration[]::new);
-
+        serviceConfiguration.sensorOrientation = cameraConfig.getSensorOrientation();
+        serviceConfiguration.lensFacing = cameraConfig.getLensFacing();
         serviceConfiguration.virtualCameraCallback = convertCallback(cameraConfig.getCallback());
         return serviceConfiguration;
     }
@@ -60,12 +61,10 @@
             @NonNull IVirtualCameraCallback camera) {
         return new android.companion.virtualcamera.IVirtualCameraCallback.Stub() {
             @Override
-            public void onStreamConfigured(
-                    int streamId, Surface surface, int width, int height, int pixelFormat)
-                    throws RemoteException {
-                VirtualCameraStreamConfig streamConfig =
-                        createStreamConfig(width, height, pixelFormat);
-                camera.onStreamConfigured(streamId, surface, streamConfig);
+            public void onStreamConfigured(int streamId, Surface surface, int width, int height,
+                    int format) throws RemoteException {
+                camera.onStreamConfigured(streamId, surface, width, height,
+                        convertToJavaFormat(format));
             }
 
             @Override
@@ -81,23 +80,30 @@
     }
 
     @NonNull
-    private static VirtualCameraStreamConfig createStreamConfig(
-            int width, int height, int pixelFormat) {
-        return new VirtualCameraStreamConfig(width, height, pixelFormat);
-    }
-
-    @NonNull
     private static SupportedStreamConfiguration convertSupportedStreamConfiguration(
             VirtualCameraStreamConfig stream) {
         SupportedStreamConfiguration supportedConfig = new SupportedStreamConfiguration();
         supportedConfig.height = stream.getHeight();
         supportedConfig.width = stream.getWidth();
-        supportedConfig.pixelFormat = convertFormat(stream.getFormat());
+        supportedConfig.pixelFormat = convertToHalFormat(stream.getFormat());
+        supportedConfig.maxFps = stream.getMaximumFramesPerSecond();
         return supportedConfig;
     }
 
-    private static int convertFormat(int format) {
-        return format == ImageFormat.YUV_420_888 ? Format.YUV_420_888 : Format.UNKNOWN;
+    private static int convertToHalFormat(int javaFormat) {
+        return switch (javaFormat) {
+            case ImageFormat.YUV_420_888 -> Format.YUV_420_888;
+            case PixelFormat.RGBA_8888 -> Format.RGBA_8888;
+            default -> Format.UNKNOWN;
+        };
+    }
+
+    private static int convertToJavaFormat(int halFormat) {
+        return switch (halFormat) {
+            case Format.YUV_420_888 -> ImageFormat.YUV_420_888;
+            case Format.RGBA_8888 -> PixelFormat.RGBA_8888;
+            default -> ImageFormat.UNKNOWN;
+        };
     }
 
     private VirtualCameraConversionUtil() {
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 0cff8b7..979449f 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -94,6 +94,8 @@
 import static android.os.Process.SHELL_UID;
 import static android.os.Process.SYSTEM_UID;
 import static android.os.Process.ZYGOTE_POLICY_FLAG_EMPTY;
+import static android.content.flags.Flags.enableBindPackageIsolatedProcess;
+
 
 import static com.android.internal.messages.nano.SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICE_BG_LAUNCH;
 import static com.android.internal.util.FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_DELEGATE;
@@ -791,13 +793,15 @@
 
     static String getProcessNameForService(ServiceInfo sInfo, ComponentName name,
             String callingPackage, String instanceName, boolean isSdkSandbox,
-            boolean inSharedIsolatedProcess) {
+            boolean inSharedIsolatedProcess, boolean inPrivateSharedIsolatedProcess) {
         if (isSdkSandbox) {
             // For SDK sandbox, the process name is passed in as the instanceName
             return instanceName;
         }
-        if ((sInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) == 0) {
-            // For regular processes, just the name in sInfo
+        if ((sInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) == 0
+                || (inPrivateSharedIsolatedProcess && !isDefaultProcessService(sInfo))) {
+            // For regular processes, or private package-shared isolated processes, just the name
+            // in sInfo
             return sInfo.processName;
         }
         // Isolated processes remain.
@@ -809,6 +813,10 @@
         }
     }
 
+    private static boolean isDefaultProcessService(ServiceInfo serviceInfo) {
+        return serviceInfo.applicationInfo.processName.equals(serviceInfo.processName);
+    }
+
     private static void traceInstant(@NonNull String message, @NonNull ServiceRecord service) {
         if (!Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
             return;
@@ -864,7 +872,7 @@
 
         ServiceLookupResult res = retrieveServiceLocked(service, instanceName, isSdkSandboxService,
                 sdkSandboxClientAppUid, sdkSandboxClientAppPackage, resolvedType, callingPackage,
-                callingPid, callingUid, userId, true, callerFg, false, false, null, false);
+                callingPid, callingUid, userId, true, callerFg, false, false, null, false, false);
         if (res == null) {
             return null;
         }
@@ -1550,7 +1558,7 @@
         ServiceLookupResult r = retrieveServiceLocked(service, instanceName, isSdkSandboxService,
                 sdkSandboxClientAppUid, sdkSandboxClientAppPackage, resolvedType, null,
                 Binder.getCallingPid(), Binder.getCallingUid(), userId, false, false, false, false,
-                null, false);
+                null, false, false);
         if (r != null) {
             if (r.record != null) {
                 final long origId = Binder.clearCallingIdentity();
@@ -1642,7 +1650,7 @@
     IBinder peekServiceLocked(Intent service, String resolvedType, String callingPackage) {
         ServiceLookupResult r = retrieveServiceLocked(service, null, resolvedType, callingPackage,
                 Binder.getCallingPid(), Binder.getCallingUid(),
-                UserHandle.getCallingUserId(), false, false, false, false, false);
+                UserHandle.getCallingUserId(), false, false, false, false, false, false);
 
         IBinder ret = null;
         if (r != null) {
@@ -3714,6 +3722,9 @@
                 || (flags & Context.BIND_EXTERNAL_SERVICE_LONG) != 0;
         final boolean allowInstant = (flags & Context.BIND_ALLOW_INSTANT) != 0;
         final boolean inSharedIsolatedProcess = (flags & Context.BIND_SHARED_ISOLATED_PROCESS) != 0;
+        final boolean inPrivateSharedIsolatedProcess =
+                ((flags & Context.BIND_PACKAGE_ISOLATED_PROCESS) != 0)
+                        && enableBindPackageIsolatedProcess();
         final boolean matchQuarantined =
                 (flags & Context.BIND_MATCH_QUARANTINED_COMPONENTS) != 0;
 
@@ -3725,7 +3736,7 @@
                 isSdkSandboxService, sdkSandboxClientAppUid, sdkSandboxClientAppPackage,
                 resolvedType, callingPackage, callingPid, callingUid, userId, true, callerFg,
                 isBindExternal, allowInstant, null /* fgsDelegateOptions */,
-                inSharedIsolatedProcess, matchQuarantined);
+                inSharedIsolatedProcess, inPrivateSharedIsolatedProcess, matchQuarantined);
         if (res == null) {
             return 0;
         }
@@ -4204,14 +4215,14 @@
     }
 
     private ServiceLookupResult retrieveServiceLocked(Intent service,
-            String instanceName, String resolvedType, String callingPackage,
-            int callingPid, int callingUid, int userId,
-            boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal,
-            boolean allowInstant, boolean inSharedIsolatedProcess) {
+            String instanceName, String resolvedType, String callingPackage, int callingPid,
+            int callingUid, int userId, boolean createIfNeeded, boolean callingFromFg,
+            boolean isBindExternal, boolean allowInstant, boolean inSharedIsolatedProcess,
+            boolean inPrivateSharedIsolatedProcess) {
         return retrieveServiceLocked(service, instanceName, false, INVALID_UID, null, resolvedType,
                 callingPackage, callingPid, callingUid, userId, createIfNeeded, callingFromFg,
                 isBindExternal, allowInstant, null /* fgsDelegateOptions */,
-                inSharedIsolatedProcess);
+                inSharedIsolatedProcess, inPrivateSharedIsolatedProcess);
     }
 
     // TODO(b/265746493): Special case for HotwordDetectionService,
@@ -4233,21 +4244,22 @@
             String callingPackage, int callingPid, int callingUid, int userId,
             boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal,
             boolean allowInstant, ForegroundServiceDelegationOptions fgsDelegateOptions,
-            boolean inSharedIsolatedProcess) {
+            boolean inSharedIsolatedProcess, boolean inPrivateSharedIsolatedProcess) {
         return retrieveServiceLocked(service, instanceName, isSdkSandboxService,
                 sdkSandboxClientAppUid, sdkSandboxClientAppPackage, resolvedType, callingPackage,
                 callingPid, callingUid, userId, createIfNeeded, callingFromFg, isBindExternal,
                 allowInstant, fgsDelegateOptions, inSharedIsolatedProcess,
-                false /* matchQuarantined */);
+                inPrivateSharedIsolatedProcess, false /* matchQuarantined */);
     }
 
-    private ServiceLookupResult retrieveServiceLocked(Intent service,
-            String instanceName, boolean isSdkSandboxService, int sdkSandboxClientAppUid,
-            String sdkSandboxClientAppPackage, String resolvedType,
+    private ServiceLookupResult retrieveServiceLocked(
+            Intent service, String instanceName, boolean isSdkSandboxService,
+            int sdkSandboxClientAppUid, String sdkSandboxClientAppPackage, String resolvedType,
             String callingPackage, int callingPid, int callingUid, int userId,
             boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal,
             boolean allowInstant, ForegroundServiceDelegationOptions fgsDelegateOptions,
-            boolean inSharedIsolatedProcess, boolean matchQuarantined) {
+            boolean inSharedIsolatedProcess, boolean inPrivateSharedIsolatedProcess,
+            boolean matchQuarantined) {
         if (isSdkSandboxService && instanceName == null) {
             throw new IllegalArgumentException("No instanceName provided for sdk sandbox process");
         }
@@ -4344,7 +4356,8 @@
                 final ServiceRestarter res = new ServiceRestarter();
                 final String processName = getProcessNameForService(sInfo, cn, callingPackage,
                         null /* instanceName */, false /* isSdkSandbox */,
-                        false /* inSharedIsolatedProcess */);
+                        false /* inSharedIsolatedProcess */,
+                        false /*inPrivateSharedIsolatedProcess*/);
                 r = new ServiceRecord(mAm, cn /* name */, cn /* instanceName */,
                         sInfo.applicationInfo.packageName, sInfo.applicationInfo.uid, filter, sInfo,
                         callingFromFg, res, processName,
@@ -4415,6 +4428,10 @@
                             throw new SecurityException("BIND_EXTERNAL_SERVICE failed, "
                                     + className + " is not exported");
                         }
+                        if (inPrivateSharedIsolatedProcess) {
+                            throw new SecurityException("BIND_PACKAGE_ISOLATED_PROCESS cannot be "
+                                    + "applied to an external service.");
+                        }
                         if ((sInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) == 0) {
                             throw new SecurityException("BIND_EXTERNAL_SERVICE failed, "
                                     + className + " is not an isolatedProcess");
@@ -4448,28 +4465,32 @@
                     throw new SecurityException("BIND_EXTERNAL_SERVICE failed, " + name +
                             " is not an externalService");
                 }
-                if (inSharedIsolatedProcess) {
+                if (inSharedIsolatedProcess && inPrivateSharedIsolatedProcess) {
+                    throw new SecurityException("Either BIND_SHARED_ISOLATED_PROCESS or "
+                            + "BIND_PACKAGE_ISOLATED_PROCESS should be set. Not both.");
+                }
+                if (inSharedIsolatedProcess || inPrivateSharedIsolatedProcess) {
                     if ((sInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) == 0) {
                         throw new SecurityException("BIND_SHARED_ISOLATED_PROCESS failed, "
                                 + className + " is not an isolatedProcess");
                     }
+                }
+                if (inPrivateSharedIsolatedProcess && isDefaultProcessService(sInfo)) {
+                    throw new SecurityException("BIND_PACKAGE_ISOLATED_PROCESS cannot be used for "
+                            + "services running in the main app process.");
+                }
+                if (inSharedIsolatedProcess) {
+                    if (instanceName == null) {
+                        throw new IllegalArgumentException("instanceName must be provided for "
+                                + "binding a service into a shared isolated process.");
+                    }
                     if ((sInfo.flags & ServiceInfo.FLAG_ALLOW_SHARED_ISOLATED_PROCESS) == 0) {
                         throw new SecurityException("BIND_SHARED_ISOLATED_PROCESS failed, "
                                 + className + " has not set the allowSharedIsolatedProcess "
                                 + " attribute.");
                     }
-                    if (instanceName == null) {
-                        throw new IllegalArgumentException("instanceName must be provided for "
-                                + "binding a service into a shared isolated process.");
-                    }
                 }
                 if (userId > 0) {
-                    if (mAm.isSystemUserOnly(sInfo.flags)) {
-                        Slog.w(TAG_SERVICE, service + " is only available for the SYSTEM user,"
-                                + " calling userId is: " + userId);
-                        return null;
-                    }
-
                     if (mAm.isSingleton(sInfo.processName, sInfo.applicationInfo,
                             sInfo.name, sInfo.flags)
                             && mAm.isValidSingletonCall(callingUid, sInfo.applicationInfo.uid)) {
@@ -4503,11 +4524,13 @@
                             = new Intent.FilterComparison(service.cloneFilter());
                     final ServiceRestarter res = new ServiceRestarter();
                     String processName = getProcessNameForService(sInfo, name, callingPackage,
-                            instanceName, isSdkSandboxService, inSharedIsolatedProcess);
+                            instanceName, isSdkSandboxService, inSharedIsolatedProcess,
+                            inPrivateSharedIsolatedProcess);
                     r = new ServiceRecord(mAm, className, name, definingPackageName,
                             definingUid, filter, sInfo, callingFromFg, res,
                             processName, sdkSandboxClientAppUid,
-                            sdkSandboxClientAppPackage, inSharedIsolatedProcess);
+                            sdkSandboxClientAppPackage,
+                            (inSharedIsolatedProcess || inPrivateSharedIsolatedProcess));
                     res.setService(r);
                     smap.mServicesByInstanceName.put(name, r);
                     smap.mServicesByIntent.put(filter, r);
@@ -8504,7 +8527,8 @@
                 null /* sdkSandboxClientAppPackage */, null /* resolvedType */, callingPackage,
                 callingPid, callingUid, userId, true /* createIfNeeded */,
                 false /* callingFromFg */, false /* isBindExternal */, false /* allowInstant */ ,
-                options, false /* inSharedIsolatedProcess */);
+                options, false /* inSharedIsolatedProcess */,
+                false /*inPrivateSharedIsolatedProcess*/);
         if (res == null || res.record == null) {
             Slog.d(TAG,
                     "startForegroundServiceDelegateLocked retrieveServiceLocked returns null");
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index e583a6c..f6d954a 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4830,7 +4830,11 @@
             if (!mConstants.mEnableWaitForFinishAttachApplication) {
                 finishAttachApplicationInner(startSeq, callingUid, pid);
             }
-            maybeSendBootCompletedLocked(app);
+
+            // Temporarily disable sending BOOT_COMPLETED to see if this was impacting perf tests
+            if (false) {
+                maybeSendBootCompletedLocked(app);
+            }
         } catch (Exception e) {
             // We need kill the process group here. (b/148588589)
             Slog.wtf(TAG, "Exception thrown during bind of " + app, e);
@@ -6525,7 +6529,24 @@
     @Override
     public int checkUriPermission(Uri uri, int pid, int uid,
             final int modeFlags, int userId, IBinder callerToken) {
-        enforceNotIsolatedCaller("checkUriPermission");
+        return checkUriPermission(uri, pid, uid, modeFlags, userId,
+                /* isFullAccessForContentUri */ false, "checkUriPermission");
+    }
+
+    /**
+     * @param uri This uri must NOT contain an embedded userId.
+     * @param userId The userId in which the uri is to be resolved.
+     */
+    @Override
+    public int checkContentUriPermissionFull(Uri uri, int pid, int uid,
+            final int modeFlags, int userId) {
+        return checkUriPermission(uri, pid, uid, modeFlags, userId,
+                /* isFullAccessForContentUri */ true, "checkContentUriPermissionFull");
+    }
+
+    private int checkUriPermission(Uri uri, int pid, int uid,
+            final int modeFlags, int userId, boolean isFullAccessForContentUri, String methodName) {
+        enforceNotIsolatedCaller(methodName);
 
         // Our own process gets to do everything.
         if (pid == MY_PID) {
@@ -6536,8 +6557,10 @@
                 return PackageManager.PERMISSION_DENIED;
             }
         }
-        return mUgmInternal.checkUriPermission(new GrantUri(userId, uri, modeFlags), uid, modeFlags)
-                ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED;
+        boolean granted = mUgmInternal.checkUriPermission(new GrantUri(userId, uri, modeFlags), uid,
+                modeFlags, isFullAccessForContentUri);
+
+        return granted ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED;
     }
 
     @Override
@@ -9858,7 +9881,7 @@
 
 
     @Override
-    public void setApplicationStartInfoCompleteListener(
+    public void addApplicationStartInfoCompleteListener(
             IApplicationStartInfoCompleteListener listener, int userId) {
         enforceNotIsolatedCaller("setApplicationStartInfoCompleteListener");
 
@@ -9873,7 +9896,8 @@
 
 
     @Override
-    public void clearApplicationStartInfoCompleteListener(int userId) {
+    public void removeApplicationStartInfoCompleteListener(
+            IApplicationStartInfoCompleteListener listener, int userId) {
         enforceNotIsolatedCaller("clearApplicationStartInfoCompleteListener");
 
         // For the simplification, we don't support USER_ALL nor USER_CURRENT here.
@@ -9882,7 +9906,8 @@
         }
 
         final int callingUid = Binder.getCallingUid();
-        mProcessList.getAppStartInfoTracker().clearStartInfoCompleteListener(callingUid, true);
+        mProcessList.getAppStartInfoTracker().removeStartInfoCompleteListener(listener, callingUid,
+                true);
     }
 
     @Override
@@ -13747,11 +13772,6 @@
         return result;
     }
 
-    boolean isSystemUserOnly(int flags) {
-        return android.multiuser.Flags.enableSystemUserOnlyForServicesAndProviders()
-                && (flags & ServiceInfo.FLAG_SYSTEM_USER_ONLY) != 0;
-    }
-
     /**
      * Checks to see if the caller is in the same app as the singleton
      * component, or the component is in a special app. It allows special apps
diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java
index 82e554e..b90e5cd 100644
--- a/services/core/java/com/android/server/am/AppStartInfoTracker.java
+++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java
@@ -119,7 +119,7 @@
 
     /** UID as key. */
     @GuardedBy("mLock")
-    private final SparseArray<ApplicationStartInfoCompleteCallback> mCallbacks;
+    private final SparseArray<ArrayList<ApplicationStartInfoCompleteCallback>> mCallbacks;
 
     /**
      * Whether or not we've loaded the historical app process start info from persistent storage.
@@ -465,11 +465,18 @@
         synchronized (mLock) {
             if (startInfo.getStartupState()
                     == ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN) {
-                ApplicationStartInfoCompleteCallback callback =
+                final List<ApplicationStartInfoCompleteCallback> callbacks =
                         mCallbacks.get(startInfo.getRealUid());
-                if (callback != null) {
-                    callback.onApplicationStartInfoComplete(startInfo);
+                if (callbacks == null) {
+                    return;
                 }
+                final int size = callbacks.size();
+                for (int i = 0; i < size; i++) {
+                    if (callbacks.get(i) != null) {
+                        callbacks.get(i).onApplicationStartInfoComplete(startInfo);
+                    }
+                }
+                mCallbacks.remove(startInfo.getRealUid());
             }
         }
     }
@@ -542,7 +549,6 @@
             } catch (RemoteException e) {
                 /*ignored*/
             }
-            clearStartInfoCompleteListener(mUid, true);
         }
 
         void unlinkToDeath() {
@@ -551,7 +557,7 @@
 
         @Override
         public void binderDied() {
-            clearStartInfoCompleteListener(mUid, false);
+            removeStartInfoCompleteListener(mCallback, mUid, false);
         }
     }
 
@@ -561,22 +567,43 @@
             if (!mEnabled) {
                 return;
             }
-            mCallbacks.put(uid, new ApplicationStartInfoCompleteCallback(listener, uid));
+            ArrayList<ApplicationStartInfoCompleteCallback> callbacks = mCallbacks.get(uid);
+            if (callbacks == null) {
+                mCallbacks.set(uid,
+                        callbacks = new ArrayList<ApplicationStartInfoCompleteCallback>());
+            }
+            callbacks.add(new ApplicationStartInfoCompleteCallback(listener, uid));
         }
     }
 
-    void clearStartInfoCompleteListener(final int uid, boolean unlinkDeathRecipient) {
+    void removeStartInfoCompleteListener(
+            final IApplicationStartInfoCompleteListener listener, final int uid,
+            boolean unlinkDeathRecipient) {
         synchronized (mLock) {
             if (!mEnabled) {
                 return;
             }
-            if (unlinkDeathRecipient) {
-                ApplicationStartInfoCompleteCallback callback = mCallbacks.get(uid);
-                if (callback != null) {
-                    callback.unlinkToDeath();
+            final ArrayList<ApplicationStartInfoCompleteCallback> callbacks = mCallbacks.get(uid);
+            if (callbacks == null) {
+                return;
+            }
+            final int size  = callbacks.size();
+            int index;
+            for (index = 0; index < size; index++) {
+                final ApplicationStartInfoCompleteCallback callback = callbacks.get(index);
+                if (callback.mCallback == listener) {
+                    if (unlinkDeathRecipient) {
+                        callback.unlinkToDeath();
+                    }
+                    break;
                 }
             }
-            mCallbacks.remove(uid);
+            if (index < size) {
+                callbacks.remove(index);
+            }
+            if (callbacks.isEmpty()) {
+                mCallbacks.remove(uid);
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java
index 30f21a6..095d907 100644
--- a/services/core/java/com/android/server/am/ContentProviderHelper.java
+++ b/services/core/java/com/android/server/am/ContentProviderHelper.java
@@ -1249,9 +1249,9 @@
             ProviderInfo cpi = providers.get(i);
             boolean singleton = mService.isSingleton(cpi.processName, cpi.applicationInfo,
                     cpi.name, cpi.flags);
-            if (isSingletonOrSystemUserOnly(cpi) && app.userId != UserHandle.USER_SYSTEM) {
-                // This is a singleton or a SYSTEM user only provider, but a user besides the
-                // SYSTEM user is asking to initialize a process it runs
+            if (singleton && app.userId != UserHandle.USER_SYSTEM) {
+                // This is a singleton provider, but a user besides the
+                // default user is asking to initialize a process it runs
                 // in...  well, no, it doesn't actually run in this process,
                 // it runs in the process of the default user.  Get rid of it.
                 providers.remove(i);
@@ -1398,7 +1398,8 @@
                                     final boolean processMatch =
                                             Objects.equals(pi.processName, app.processName)
                                             || pi.multiprocess;
-                                    final boolean userMatch = !isSingletonOrSystemUserOnly(pi)
+                                    final boolean userMatch = !mService.isSingleton(
+                                            pi.processName, pi.applicationInfo, pi.name, pi.flags)
                                             || app.userId == UserHandle.USER_SYSTEM;
                                     final boolean isInstantApp = pi.applicationInfo.isInstantApp();
                                     final boolean splitInstalled = pi.splitName == null
@@ -1984,13 +1985,4 @@
             return isAuthRedirected;
         }
     }
-
-    /**
-     * Returns true if Provider is either singleUser or systemUserOnly provider.
-     */
-    private boolean isSingletonOrSystemUserOnly(ProviderInfo pi) {
-        return (android.multiuser.Flags.enableSystemUserOnlyForServicesAndProviders()
-                && mService.isSystemUserOnly(pi.flags))
-                || mService.isSingleton(pi.processName, pi.applicationInfo, pi.name, pi.flags);
-    }
 }
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index be48eb4..1ae2559 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -100,6 +100,10 @@
             Flags.FLAG_ENABLE_VSYNC_LOW_POWER_VOTE,
             Flags::enableVsyncLowPowerVote);
 
+    private final FlagState mVsyncLowLightVote = new FlagState(
+            Flags.FLAG_ENABLE_VSYNC_LOW_LIGHT_VOTE,
+            Flags::enableVsyncLowLightVote);
+
     private final FlagState mBrightnessWearBedtimeModeClamperFlagState = new FlagState(
             Flags.FLAG_BRIGHTNESS_WEAR_BEDTIME_MODE_CLAMPER,
             Flags::brightnessWearBedtimeModeClamper);
@@ -220,6 +224,10 @@
         return mVsyncLowPowerVote.isEnabled();
     }
 
+    public boolean isVsyncLowLightVoteEnabled() {
+        return mVsyncLowLightVote.isEnabled();
+    }
+
     public boolean isBrightnessWearBedtimeModeClamperEnabled() {
         return mBrightnessWearBedtimeModeClamperFlagState.isEnabled();
     }
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index a2319a8..c2f52b5 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -146,6 +146,14 @@
 }
 
 flag {
+    name: "enable_vsync_low_light_vote"
+    namespace: "display_manager"
+    description: "Feature flag for vsync low light vote"
+    bug: "314921657"
+    is_fixed_read_only: true
+}
+
+flag {
     name: "brightness_wear_bedtime_mode_clamper"
     namespace: "display_manager"
     description: "Feature flag for the Wear Bedtime mode brightness clamper"
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index ad3deff..8707000 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -215,7 +215,8 @@
         mDeviceConfigDisplaySettings = new DeviceConfigDisplaySettings();
         mSettingsObserver = new SettingsObserver(context, handler, mDvrrSupported,
                 displayManagerFlags);
-        mBrightnessObserver = new BrightnessObserver(context, handler, injector);
+        mBrightnessObserver = new BrightnessObserver(context, handler, injector, mDvrrSupported,
+                displayManagerFlags);
         mDefaultDisplayDeviceConfig = null;
         mUdfpsObserver = new UdfpsObserver();
         mVotesStorage = new VotesStorage(this::notifyDesiredDisplayModeSpecsChangedLocked,
@@ -1510,6 +1511,8 @@
         private final Injector mInjector;
         private final Handler mHandler;
 
+        private final boolean mVsyncLowLightBlockingVoteEnabled;
+
         private final IThermalEventListener.Stub mThermalListener =
                 new IThermalEventListener.Stub() {
                     @Override
@@ -1544,7 +1547,8 @@
         @GuardedBy("mLock")
         private @Temperature.ThrottlingStatus int mThermalStatus = Temperature.THROTTLING_NONE;
 
-        BrightnessObserver(Context context, Handler handler, Injector injector) {
+        BrightnessObserver(Context context, Handler handler, Injector injector,
+                boolean dvrrSupported , DisplayManagerFlags flags) {
             mContext = context;
             mHandler = handler;
             mInjector = injector;
@@ -1552,6 +1556,7 @@
                 /* attemptReadFromFeatureParams= */ false);
             mRefreshRateInHighZone = context.getResources().getInteger(
                     R.integer.config_fixedRefreshRateInHighZone);
+            mVsyncLowLightBlockingVoteEnabled = dvrrSupported && flags.isVsyncLowLightVoteEnabled();
         }
 
         /**
@@ -2131,7 +2136,17 @@
                                 Vote.forPhysicalRefreshRates(range.min, range.max);
                     }
                 }
-                refreshRateSwitchingVote = Vote.forDisableRefreshRateSwitching();
+
+                if (mVsyncLowLightBlockingVoteEnabled) {
+                    refreshRateSwitchingVote = Vote.forSupportedModesAndDisableRefreshRateSwitching(
+                            List.of(
+                                    new SupportedModesVote.SupportedMode(
+                                            /* peakRefreshRate= */ 60f, /* vsyncRate= */ 60f),
+                                    new SupportedModesVote.SupportedMode(
+                                            /* peakRefreshRate= */120f, /* vsyncRate= */ 120f)));
+                } else {
+                    refreshRateSwitchingVote = Vote.forDisableRefreshRateSwitching();
+                }
             }
 
             boolean insideHighZone = hasValidHighZone()
diff --git a/services/core/java/com/android/server/display/mode/Vote.java b/services/core/java/com/android/server/display/mode/Vote.java
index 8f39570..e8d5a19 100644
--- a/services/core/java/com/android/server/display/mode/Vote.java
+++ b/services/core/java/com/android/server/display/mode/Vote.java
@@ -170,6 +170,13 @@
         return new SupportedModesVote(supportedModes);
     }
 
+
+    static Vote forSupportedModesAndDisableRefreshRateSwitching(
+            List<SupportedModesVote.SupportedMode> supportedModes) {
+        return new CombinedVote(
+                List.of(forDisableRefreshRateSwitching(), forSupportedModes(supportedModes)));
+    }
+
     static String priorityToString(int priority) {
         switch (priority) {
             case PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE:
diff --git a/services/core/java/com/android/server/media/MediaFeatureFlagManager.java b/services/core/java/com/android/server/media/MediaFeatureFlagManager.java
deleted file mode 100644
index f90f64a..0000000
--- a/services/core/java/com/android/server/media/MediaFeatureFlagManager.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.media;
-
-import android.annotation.StringDef;
-import android.app.ActivityThread;
-import android.app.Application;
-import android.provider.DeviceConfig;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/* package */ class MediaFeatureFlagManager {
-
-    /**
-     * Namespace for media better together features.
-     */
-    private static final String NAMESPACE_MEDIA_BETTER_TOGETHER = "media_better_together";
-
-    @StringDef(
-            prefix = "FEATURE_",
-            value = {
-                FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE
-            })
-    @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
-    @Retention(RetentionPolicy.SOURCE)
-    /* package */ @interface MediaFeatureFlag {}
-
-    /**
-     * Whether to use IMPORTANCE_FOREGROUND (i.e. 100) or IMPORTANCE_FOREGROUND_SERVICE (i.e. 125)
-     * as the minimum package importance for scanning.
-     */
-    /* package */ static final @MediaFeatureFlag String
-            FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE = "scanning_package_minimum_importance";
-
-    private static final MediaFeatureFlagManager sInstance = new MediaFeatureFlagManager();
-
-    private MediaFeatureFlagManager() {
-        // Empty to prevent instantiation.
-    }
-
-    /* package */ static MediaFeatureFlagManager getInstance() {
-        return sInstance;
-    }
-
-    /**
-     * Returns a boolean value from {@link DeviceConfig} from the system_time namespace, or
-     * {@code defaultValue} if there is no explicit value set.
-     */
-    public boolean getBoolean(@MediaFeatureFlag String key, boolean defaultValue) {
-        return DeviceConfig.getBoolean(NAMESPACE_MEDIA_BETTER_TOGETHER, key, defaultValue);
-    }
-
-    /**
-     * Returns an int value from {@link DeviceConfig} from the system_time namespace, or {@code
-     * defaultValue} if there is no explicit value set.
-     */
-    public int getInt(@MediaFeatureFlag String key, int defaultValue) {
-        return DeviceConfig.getInt(NAMESPACE_MEDIA_BETTER_TOGETHER, key, defaultValue);
-    }
-
-    /**
-     * Adds a listener to react for changes in media feature flags values. Future calls to this
-     * method with the same listener will replace the old namespace and executor.
-     *
-     * @param onPropertiesChangedListener The listener to add.
-     */
-    public void addOnPropertiesChangedListener(
-            DeviceConfig.OnPropertiesChangedListener onPropertiesChangedListener) {
-        Application currentApplication = ActivityThread.currentApplication();
-        if (currentApplication != null) {
-            DeviceConfig.addOnPropertiesChangedListener(
-                    NAMESPACE_MEDIA_BETTER_TOGETHER,
-                    currentApplication.getMainExecutor(),
-                    onPropertiesChangedListener);
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index e048522..28a1c7a 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -16,7 +16,7 @@
 
 package com.android.server.media;
 
-import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
 import static android.content.Intent.ACTION_SCREEN_OFF;
 import static android.content.Intent.ACTION_SCREEN_ON;
 import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR;
@@ -24,7 +24,6 @@
 import static android.media.MediaRouter2Utils.getProviderId;
 
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
-import static com.android.server.media.MediaFeatureFlagManager.FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE;
 
 import android.Manifest;
 import android.annotation.NonNull;
@@ -55,7 +54,6 @@
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.UserHandle;
-import android.provider.DeviceConfig;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -97,11 +95,7 @@
     //       in MediaRouter2, remove this constant and replace the usages with the real request IDs.
     private static final long DUMMY_REQUEST_ID = -1;
 
-    private static int sPackageImportanceForScanning =
-            MediaFeatureFlagManager.getInstance()
-                    .getInt(
-                            FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE,
-                            IMPORTANCE_FOREGROUND_SERVICE);
+    private static final int REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING = IMPORTANCE_FOREGROUND;
 
     /**
      * Contains the list of bluetooth permissions that are required to do system routing.
@@ -159,7 +153,7 @@
         mContext = context;
         mActivityManager = mContext.getSystemService(ActivityManager.class);
         mActivityManager.addOnUidImportanceListener(mOnUidImportanceListener,
-                sPackageImportanceForScanning);
+                REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING);
         mPowerManager = mContext.getSystemService(PowerManager.class);
         mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
 
@@ -171,9 +165,6 @@
         }
 
         mContext.getPackageManager().addOnPermissionsChangeListener(this::onPermissionsChanged);
-
-        MediaFeatureFlagManager.getInstance()
-                .addOnPropertiesChangedListener(this::onDeviceConfigChange);
     }
 
     /**
@@ -1443,8 +1434,13 @@
             return;
         }
 
-        Slog.i(TAG, TextUtils.formatSimple(
-                "startScan | manager: %d", managerRecord.mManagerId));
+        Slog.i(
+                TAG,
+                TextUtils.formatSimple(
+                        "startScan | manager: %d, ownerPackageName: %s, targetPackageName: %s",
+                        managerRecord.mManagerId,
+                        managerRecord.mOwnerPackageName,
+                        managerRecord.mTargetPackageName));
 
         managerRecord.startScan();
     }
@@ -1457,8 +1453,13 @@
             return;
         }
 
-        Slog.i(TAG, TextUtils.formatSimple(
-                "stopScan | manager: %d", managerRecord.mManagerId));
+        Slog.i(
+                TAG,
+                TextUtils.formatSimple(
+                        "stopScan | manager: %d, ownerPackageName: %s, targetPackageName: %s",
+                        managerRecord.mManagerId,
+                        managerRecord.mOwnerPackageName,
+                        managerRecord.mTargetPackageName));
 
         managerRecord.stopScan();
     }
@@ -1725,13 +1726,6 @@
 
     // End of locked methods that are used by both MediaRouter2 and MediaRouter2Manager.
 
-    private void onDeviceConfigChange(@NonNull DeviceConfig.Properties properties) {
-        sPackageImportanceForScanning =
-                properties.getInt(
-                        /* name */ FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE,
-                        /* defaultValue */ IMPORTANCE_FOREGROUND_SERVICE);
-    }
-
     static long toUniqueRequestId(int requesterId, int originalRequestId) {
         return ((long) requesterId << 32) | originalRequestId;
     }
@@ -3170,7 +3164,7 @@
                             record ->
                                     service.mActivityManager.getPackageImportance(
                                                     record.mPackageName)
-                                            <= sPackageImportanceForScanning)
+                                            <= REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING)
                     .collect(Collectors.toList());
         }
 
@@ -3187,7 +3181,7 @@
                                     manager.mIsScanning
                                             && service.mActivityManager.getPackageImportance(
                                                             manager.mOwnerPackageName)
-                                                    <= sPackageImportanceForScanning);
+                                                    <= REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING);
         }
 
         private MediaRoute2Provider findProvider(@Nullable String providerId) {
diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java
index 659c36c..5d71439e 100644
--- a/services/core/java/com/android/server/pm/ApexManager.java
+++ b/services/core/java/com/android/server/pm/ApexManager.java
@@ -276,7 +276,8 @@
      * Returns list of {@code packageName} of apks inside the given apex.
      * @param apexPackageName Package name of the apk container of apex
      */
-    abstract List<String> getApksInApex(String apexPackageName);
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public abstract List<String> getApksInApex(String apexPackageName);
 
     /**
      * Returns the apex module name for the given package name, if the package is an APEX. Otherwise
@@ -751,8 +752,9 @@
             }
         }
 
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
         @Override
-        List<String> getApksInApex(String apexPackageName) {
+        public List<String> getApksInApex(String apexPackageName) {
             synchronized (mLock) {
                 Preconditions.checkState(mPackageNameToApexModuleName != null,
                         "APEX packages have not been scanned");
diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java b/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java
index e54b40e..03c75e0 100644
--- a/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java
+++ b/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java
@@ -37,7 +37,17 @@
     void revokeUriPermission(String targetPackage, int callingUid,
             GrantUri grantUri, final int modeFlags);
 
-    boolean checkUriPermission(GrantUri grantUri, int uid, final int modeFlags);
+    /**
+     * Check if the uid has permission to the URI in grantUri.
+     *
+     * @param isFullAccessForContentUri If true, the URI has to be a content URI
+     *                                  and the method will consider full access.
+     *                                  Otherwise, the method will only consider
+     *                                  URI grants.
+     */
+    boolean checkUriPermission(GrantUri grantUri, int uid, int modeFlags,
+            boolean isFullAccessForContentUri);
+
     int checkGrantUriPermission(
             int callingUid, String targetPkg, Uri uri, int modeFlags, int userId);
 
diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerService.java b/services/core/java/com/android/server/uri/UriGrantsManagerService.java
index e501b9d..ce2cbed 100644
--- a/services/core/java/com/android/server/uri/UriGrantsManagerService.java
+++ b/services/core/java/com/android/server/uri/UriGrantsManagerService.java
@@ -25,6 +25,7 @@
 import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
 import static android.content.pm.PackageManager.MATCH_ANY_USER;
 import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AUTO;
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
 import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
@@ -1103,7 +1104,8 @@
      */
     private int checkGrantUriPermissionUnlocked(int callingUid, String targetPkg, GrantUri grantUri,
             int modeFlags, int lastTargetUid) {
-        if (!Intent.isAccessUriMode(modeFlags)) {
+        if (!isContentUriWithAccessModeFlags(grantUri, modeFlags,
+                /* logAction */ "grant URI permission")) {
             return -1;
         }
 
@@ -1111,12 +1113,6 @@
             if (DEBUG) Slog.v(TAG, "Checking grant " + targetPkg + " permission to " + grantUri);
         }
 
-        // If this is not a content: uri, we can't do anything with it.
-        if (!ContentResolver.SCHEME_CONTENT.equals(grantUri.uri.getScheme())) {
-            if (DEBUG) Slog.v(TAG, "Can't grant URI permission for non-content URI: " + grantUri);
-            return -1;
-        }
-
         // Bail early if system is trying to hand out permissions directly; it
         // must always grant permissions on behalf of someone explicit.
         final int callingAppId = UserHandle.getAppId(callingUid);
@@ -1137,7 +1133,7 @@
 
         final String authority = grantUri.uri.getAuthority();
         final ProviderInfo pi = getProviderInfo(authority, grantUri.sourceUserId,
-                MATCH_DEBUG_TRIAGED_MISSING, callingUid);
+                MATCH_DIRECT_BOOT_AUTO, callingUid);
         if (pi == null) {
             Slog.w(TAG, "No content provider found for permission check: " +
                     grantUri.uri.toSafeString());
@@ -1285,6 +1281,65 @@
         return targetUid;
     }
 
+    private boolean isContentUriWithAccessModeFlags(GrantUri grantUri, int modeFlags,
+            String logAction) {
+        if (!Intent.isAccessUriMode(modeFlags)) {
+            if (DEBUG) Slog.v(TAG, "Mode flags are not access URI mode flags: " + modeFlags);
+            return false;
+        }
+
+        if (!ContentResolver.SCHEME_CONTENT.equals(grantUri.uri.getScheme())) {
+            if (DEBUG) {
+                Slog.v(TAG, "Can't " + logAction + " on non-content URI: " + grantUri);
+            }
+            return false;
+        }
+
+        return true;
+    }
+
+    /** Check if the uid has permission to the content URI in grantUri. */
+    private boolean checkContentUriPermissionFullUnlocked(GrantUri grantUri, int uid,
+            int modeFlags) {
+        if (uid < 0) {
+            throw new IllegalArgumentException("Uid must be positive for the content URI "
+                    + "permission check of " + grantUri.uri.toSafeString());
+        }
+
+        if (!isContentUriWithAccessModeFlags(grantUri, modeFlags,
+                /* logAction */ "check content URI permission")) {
+            throw new IllegalArgumentException("The URI must be a content URI and the mode "
+                    + "flags must be at least read and/or write for the content URI permission "
+                    + "check of " + grantUri.uri.toSafeString());
+        }
+
+        final int appId = UserHandle.getAppId(uid);
+        if ((appId == SYSTEM_UID) || (appId == ROOT_UID)) {
+            return true;
+        }
+
+        // Retrieve the URI's content provider
+        final String authority = grantUri.uri.getAuthority();
+        ProviderInfo pi = getProviderInfo(authority, grantUri.sourceUserId, MATCH_DIRECT_BOOT_AUTO,
+                uid);
+
+        if (pi == null) {
+            Slog.w(TAG, "No content provider found for content URI permission check: "
+                    + grantUri.uri.toSafeString());
+            return false;
+        }
+
+        // Check if it has general permission to the URI's content provider
+        if (checkHoldingPermissionsUnlocked(pi, grantUri, uid, modeFlags)) {
+            return true;
+        }
+
+        // Check if it has explicitly granted permissions to the URI
+        synchronized (mLock) {
+            return checkUriPermissionLocked(grantUri, uid, modeFlags);
+        }
+    }
+
     /**
      * @param userId The userId in which the uri is to be resolved.
      */
@@ -1482,7 +1537,12 @@
         }
 
         @Override
-        public boolean checkUriPermission(GrantUri grantUri, int uid, int modeFlags) {
+        public boolean checkUriPermission(GrantUri grantUri, int uid, int modeFlags,
+                boolean isFullAccessForContentUri) {
+            if (isFullAccessForContentUri) {
+                return UriGrantsManagerService.this.checkContentUriPermissionFullUnlocked(grantUri,
+                        uid, modeFlags);
+            }
             synchronized (mLock) {
                 return UriGrantsManagerService.this.checkUriPermissionLocked(grantUri, uid,
                         modeFlags);
diff --git a/services/core/java/com/android/server/wm/LegacyTransitionTracer.java b/services/core/java/com/android/server/wm/LegacyTransitionTracer.java
new file mode 100644
index 0000000..fb2d5be
--- /dev/null
+++ b/services/core/java/com/android/server/wm/LegacyTransitionTracer.java
@@ -0,0 +1,331 @@
+/*
+ * 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.wm;
+
+import static android.os.Build.IS_USER;
+
+import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER;
+import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_H;
+import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_L;
+import static com.android.server.wm.shell.TransitionTraceProto.REAL_TO_ELAPSED_TIME_OFFSET_NANOS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.util.Log;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.TraceBuffer;
+import com.android.server.wm.Transition.ChangeInfo;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Helper class to collect and dump transition traces.
+ */
+class LegacyTransitionTracer implements TransitionTracer {
+
+    private static final String LOG_TAG = "TransitionTracer";
+
+    private static final int ALWAYS_ON_TRACING_CAPACITY = 15 * 1024; // 15 KB
+    private static final int ACTIVE_TRACING_BUFFER_CAPACITY = 5000 * 1024; // 5 MB
+
+    // This will be the size the proto output streams are initialized to.
+    // Ideally this should fit most or all the proto objects we will create and be no bigger than
+    // that to ensure to don't use excessive amounts of memory.
+    private static final int CHUNK_SIZE = 64;
+
+    static final String WINSCOPE_EXT = ".winscope";
+    private static final String TRACE_FILE =
+            "/data/misc/wmtrace/wm_transition_trace" + WINSCOPE_EXT;
+    private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
+
+    private final TraceBuffer mTraceBuffer = new TraceBuffer(ALWAYS_ON_TRACING_CAPACITY);
+
+    private final Object mEnabledLock = new Object();
+    private volatile boolean mActiveTracingEnabled = false;
+
+    /**
+     * Records key information about a transition that has been sent to Shell to be played.
+     * More information will be appended to the same proto object once the transition is finished or
+     * aborted.
+     * Transition information won't be added to the trace buffer until
+     * {@link #logFinishedTransition} or {@link #logAbortedTransition} is called for this
+     * transition.
+     *
+     * @param transition The transition that has been sent to Shell.
+     * @param targets Information about the target windows of the transition.
+     */
+    @Override
+    public void logSentTransition(Transition transition, ArrayList<ChangeInfo> targets) {
+        try {
+            final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
+            final long protoToken = outputStream
+                    .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
+            outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
+            outputStream.write(com.android.server.wm.shell.Transition.CREATE_TIME_NS,
+                    transition.mLogger.mCreateTimeNs);
+            outputStream.write(com.android.server.wm.shell.Transition.SEND_TIME_NS,
+                    transition.mLogger.mSendTimeNs);
+            outputStream.write(com.android.server.wm.shell.Transition.START_TRANSACTION_ID,
+                    transition.getStartTransaction().getId());
+            outputStream.write(com.android.server.wm.shell.Transition.FINISH_TRANSACTION_ID,
+                    transition.getFinishTransaction().getId());
+            dumpTransitionTargetsToProto(outputStream, transition, targets);
+            outputStream.end(protoToken);
+
+            mTraceBuffer.add(outputStream);
+        } catch (Exception e) {
+            // Don't let any errors in the tracing cause the transition to fail
+            Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
+        }
+    }
+
+    /**
+     * Completes the information dumped in {@link #logSentTransition} for a transition
+     * that has finished or aborted, and add the proto object to the trace buffer.
+     *
+     * @param transition The transition that has finished.
+     */
+    @Override
+    public void logFinishedTransition(Transition transition) {
+        try {
+            final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
+            final long protoToken = outputStream
+                    .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
+            outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
+            outputStream.write(com.android.server.wm.shell.Transition.FINISH_TIME_NS,
+                    transition.mLogger.mFinishTimeNs);
+            outputStream.end(protoToken);
+
+            mTraceBuffer.add(outputStream);
+        } catch (Exception e) {
+            // Don't let any errors in the tracing cause the transition to fail
+            Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
+        }
+    }
+
+    /**
+     * Same as {@link #logFinishedTransition} but don't add the transition to the trace buffer
+     * unless actively tracing.
+     *
+     * @param transition The transition that has been aborted
+     */
+    @Override
+    public void logAbortedTransition(Transition transition) {
+        try {
+            final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
+            final long protoToken = outputStream
+                    .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
+            outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
+            outputStream.write(com.android.server.wm.shell.Transition.ABORT_TIME_NS,
+                    transition.mLogger.mAbortTimeNs);
+            outputStream.end(protoToken);
+
+            mTraceBuffer.add(outputStream);
+        } catch (Exception e) {
+            // Don't let any errors in the tracing cause the transition to fail
+            Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
+        }
+    }
+
+    @Override
+    public void logRemovingStartingWindow(@NonNull StartingData startingData) {
+        if (startingData.mTransitionId == 0) {
+            return;
+        }
+        try {
+            final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
+            final long protoToken = outputStream
+                    .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
+            outputStream.write(com.android.server.wm.shell.Transition.ID,
+                    startingData.mTransitionId);
+            outputStream.write(
+                    com.android.server.wm.shell.Transition.STARTING_WINDOW_REMOVE_TIME_NS,
+                    SystemClock.elapsedRealtimeNanos());
+            outputStream.end(protoToken);
+
+            mTraceBuffer.add(outputStream);
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
+        }
+    }
+
+    private void dumpTransitionTargetsToProto(ProtoOutputStream outputStream,
+            Transition transition, ArrayList<ChangeInfo> targets) {
+        Trace.beginSection("TransitionTracer#dumpTransitionTargetsToProto");
+        if (mActiveTracingEnabled) {
+            outputStream.write(com.android.server.wm.shell.Transition.ID,
+                    transition.getSyncId());
+        }
+
+        outputStream.write(com.android.server.wm.shell.Transition.TYPE, transition.mType);
+        outputStream.write(com.android.server.wm.shell.Transition.FLAGS, transition.getFlags());
+
+        for (int i = 0; i < targets.size(); ++i) {
+            final long changeToken = outputStream
+                    .start(com.android.server.wm.shell.Transition.TARGETS);
+
+            final Transition.ChangeInfo target = targets.get(i);
+
+            final int layerId;
+            if (target.mContainer.mSurfaceControl.isValid()) {
+                layerId = target.mContainer.mSurfaceControl.getLayerId();
+            } else {
+                layerId = -1;
+            }
+
+            outputStream.write(com.android.server.wm.shell.Target.MODE, target.mReadyMode);
+            outputStream.write(com.android.server.wm.shell.Target.FLAGS, target.mReadyFlags);
+            outputStream.write(com.android.server.wm.shell.Target.LAYER_ID, layerId);
+
+            if (mActiveTracingEnabled) {
+                // What we use in the WM trace
+                final int windowId = System.identityHashCode(target.mContainer);
+                outputStream.write(com.android.server.wm.shell.Target.WINDOW_ID, windowId);
+            }
+
+            outputStream.end(changeToken);
+        }
+
+        Trace.endSection();
+    }
+
+    /**
+     * Starts collecting transitions for the trace.
+     * If called while a trace is already running, this will reset the trace.
+     */
+    @Override
+    public void startTrace(@Nullable PrintWriter pw) {
+        if (IS_USER) {
+            LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
+            return;
+        }
+        Trace.beginSection("TransitionTracer#startTrace");
+        LogAndPrintln.i(pw, "Starting shell transition trace.");
+        synchronized (mEnabledLock) {
+            mActiveTracingEnabled = true;
+            mTraceBuffer.resetBuffer();
+            mTraceBuffer.setCapacity(ACTIVE_TRACING_BUFFER_CAPACITY);
+        }
+        Trace.endSection();
+    }
+
+    /**
+     * Stops collecting the transition trace and dump to trace to file.
+     *
+     * Dumps the trace to @link{TRACE_FILE}.
+     */
+    @Override
+    public void stopTrace(@Nullable PrintWriter pw) {
+        stopTrace(pw, new File(TRACE_FILE));
+    }
+
+    /**
+     * Stops collecting the transition trace and dump to trace to file.
+     * @param outputFile The file to dump the transition trace to.
+     */
+    public void stopTrace(@Nullable PrintWriter pw, File outputFile) {
+        if (IS_USER) {
+            LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
+            return;
+        }
+        Trace.beginSection("TransitionTracer#stopTrace");
+        LogAndPrintln.i(pw, "Stopping shell transition trace.");
+        synchronized (mEnabledLock) {
+            mActiveTracingEnabled = false;
+            writeTraceToFileLocked(pw, outputFile);
+            mTraceBuffer.resetBuffer();
+            mTraceBuffer.setCapacity(ALWAYS_ON_TRACING_CAPACITY);
+        }
+        Trace.endSection();
+    }
+
+    /**
+     * Being called while taking a bugreport so that tracing files can be included in the bugreport.
+     *
+     * @param pw Print writer
+     */
+    @Override
+    public void saveForBugreport(@Nullable PrintWriter pw) {
+        if (IS_USER) {
+            LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
+            return;
+        }
+        Trace.beginSection("TransitionTracer#saveForBugreport");
+        synchronized (mEnabledLock) {
+            final File outputFile = new File(TRACE_FILE);
+            writeTraceToFileLocked(pw, outputFile);
+        }
+        Trace.endSection();
+    }
+
+    @Override
+    public boolean isTracing() {
+        return mActiveTracingEnabled;
+    }
+
+    private void writeTraceToFileLocked(@Nullable PrintWriter pw, File file) {
+        Trace.beginSection("TransitionTracer#writeTraceToFileLocked");
+        try {
+            ProtoOutputStream proto = new ProtoOutputStream(CHUNK_SIZE);
+            proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE);
+            long timeOffsetNs =
+                    TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis())
+                            - SystemClock.elapsedRealtimeNanos();
+            proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs);
+            int pid = android.os.Process.myPid();
+            LogAndPrintln.i(pw, "Writing file to " + file.getAbsolutePath()
+                    + " from process " + pid);
+            mTraceBuffer.writeTraceToFile(file, proto);
+        } catch (IOException e) {
+            LogAndPrintln.e(pw, "Unable to write buffer to file", e);
+        }
+        Trace.endSection();
+    }
+
+    private static class LogAndPrintln {
+        private static void i(@Nullable PrintWriter pw, String msg) {
+            Log.i(LOG_TAG, msg);
+            if (pw != null) {
+                pw.println(msg);
+                pw.flush();
+            }
+        }
+
+        private static void e(@Nullable PrintWriter pw, String msg) {
+            Log.e(LOG_TAG, msg);
+            if (pw != null) {
+                pw.println("ERROR: " + msg);
+                pw.flush();
+            }
+        }
+
+        private static void e(@Nullable PrintWriter pw, String msg, @NonNull Exception e) {
+            Log.e(LOG_TAG, msg, e);
+            if (pw != null) {
+                pw.println("ERROR: " + msg + " ::\n " + e);
+                pw.flush();
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java b/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java
new file mode 100644
index 0000000..eae9951
--- /dev/null
+++ b/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.NonNull;
+import android.internal.perfetto.protos.PerfettoTrace;
+import android.os.SystemClock;
+import android.tracing.perfetto.DataSourceParams;
+import android.tracing.perfetto.InitArguments;
+import android.tracing.perfetto.Producer;
+import android.tracing.transition.TransitionDataSource;
+import android.util.proto.ProtoOutputStream;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+
+class PerfettoTransitionTracer implements TransitionTracer {
+    private final AtomicInteger mActiveTraces = new AtomicInteger(0);
+    private final TransitionDataSource mDataSource =
+            new TransitionDataSource(this.mActiveTraces::incrementAndGet, () -> {},
+                    this.mActiveTraces::decrementAndGet);
+
+    PerfettoTransitionTracer() {
+        Producer.init(InitArguments.DEFAULTS);
+        mDataSource.register(DataSourceParams.DEFAULTS);
+    }
+
+    /**
+     * Records key information about a transition that has been sent to Shell to be played.
+     * More information will be appended to the same proto object once the transition is finished or
+     * aborted.
+     * Transition information won't be added to the trace buffer until
+     * {@link #logFinishedTransition} or {@link #logAbortedTransition} is called for this
+     * transition.
+     *
+     * @param transition The transition that has been sent to Shell.
+     * @param targets Information about the target windows of the transition.
+     */
+    @Override
+    public void logSentTransition(Transition transition, ArrayList<Transition.ChangeInfo> targets) {
+        if (!isTracing()) {
+            return;
+        }
+
+        mDataSource.trace((ctx) -> {
+            final ProtoOutputStream os = ctx.newTracePacket();
+
+            final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
+
+            os.write(PerfettoTrace.ShellTransition.ID, transition.getSyncId());
+            os.write(PerfettoTrace.ShellTransition.CREATE_TIME_NS,
+                    transition.mLogger.mCreateTimeNs);
+            os.write(PerfettoTrace.ShellTransition.SEND_TIME_NS, transition.mLogger.mSendTimeNs);
+            os.write(PerfettoTrace.ShellTransition.START_TRANSACTION_ID,
+                    transition.getStartTransaction().getId());
+            os.write(PerfettoTrace.ShellTransition.FINISH_TRANSACTION_ID,
+                    transition.getFinishTransaction().getId());
+            os.write(PerfettoTrace.ShellTransition.TYPE, transition.mType);
+            os.write(PerfettoTrace.ShellTransition.FLAGS, transition.getFlags());
+
+            addTransitionTargetsToProto(os, targets);
+
+            os.end(token);
+        });
+    }
+
+    /**
+     * Completes the information dumped in {@link #logSentTransition} for a transition
+     * that has finished or aborted, and add the proto object to the trace buffer.
+     *
+     * @param transition The transition that has finished.
+     */
+    @Override
+    public void logFinishedTransition(Transition transition) {
+        if (!isTracing()) {
+            return;
+        }
+
+        mDataSource.trace((ctx) -> {
+            final ProtoOutputStream os = ctx.newTracePacket();
+
+            final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
+            os.write(PerfettoTrace.ShellTransition.ID, transition.getSyncId());
+            os.write(PerfettoTrace.ShellTransition.FINISH_TIME_NS,
+                    transition.mLogger.mFinishTimeNs);
+            os.end(token);
+        });
+    }
+
+    /**
+     * Same as {@link #logFinishedTransition} but don't add the transition to the trace buffer
+     * unless actively tracing.
+     *
+     * @param transition The transition that has been aborted
+     */
+    @Override
+    public void logAbortedTransition(Transition transition) {
+        if (!isTracing()) {
+            return;
+        }
+
+        mDataSource.trace((ctx) -> {
+            final ProtoOutputStream os = ctx.newTracePacket();
+
+            final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
+            os.write(PerfettoTrace.ShellTransition.ID, transition.getSyncId());
+            os.write(PerfettoTrace.ShellTransition.WM_ABORT_TIME_NS,
+                    transition.mLogger.mAbortTimeNs);
+            os.end(token);
+        });
+    }
+
+    @Override
+    public void logRemovingStartingWindow(@NonNull StartingData startingData) {
+        if (!isTracing()) {
+            return;
+        }
+
+        mDataSource.trace((ctx) -> {
+            final ProtoOutputStream os = ctx.newTracePacket();
+
+            final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
+            os.write(PerfettoTrace.ShellTransition.ID, startingData.mTransitionId);
+            os.write(PerfettoTrace.ShellTransition.STARTING_WINDOW_REMOVE_TIME_NS,
+                    SystemClock.elapsedRealtimeNanos());
+            os.end(token);
+        });
+    }
+
+    @Override
+    public void startTrace(PrintWriter pw) {
+        // No-op
+    }
+
+    @Override
+    public void stopTrace(PrintWriter pw) {
+        // No-op
+    }
+
+    @Override
+    public void saveForBugreport(PrintWriter pw) {
+        // Nothing to do here. Handled by Perfetto.
+    }
+
+    @Override
+    public boolean isTracing() {
+        return mActiveTraces.get() > 0;
+    }
+
+    private void addTransitionTargetsToProto(
+            ProtoOutputStream os,
+            ArrayList<Transition.ChangeInfo> targets
+    ) {
+        for (int i = 0; i < targets.size(); ++i) {
+            final Transition.ChangeInfo target = targets.get(i);
+
+            final int layerId;
+            if (target.mContainer.mSurfaceControl.isValid()) {
+                layerId = target.mContainer.mSurfaceControl.getLayerId();
+            } else {
+                layerId = -1;
+            }
+            final int windowId = System.identityHashCode(target.mContainer);
+
+            final long token = os.start(PerfettoTrace.ShellTransition.TARGETS);
+            os.write(PerfettoTrace.ShellTransition.Target.MODE, target.mReadyMode);
+            os.write(PerfettoTrace.ShellTransition.Target.FLAGS, target.mReadyFlags);
+            os.write(PerfettoTrace.ShellTransition.Target.LAYER_ID, layerId);
+            os.write(PerfettoTrace.ShellTransition.Target.WINDOW_ID, windowId);
+            os.end(token);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/TransitionTracer.java b/services/core/java/com/android/server/wm/TransitionTracer.java
index c59d2d3..0f3fe22 100644
--- a/services/core/java/com/android/server/wm/TransitionTracer.java
+++ b/services/core/java/com/android/server/wm/TransitionTracer.java
@@ -1,323 +1,19 @@
-/*
- * 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.wm;
 
-import static android.os.Build.IS_USER;
-
-import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER;
-import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_H;
-import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_L;
-import static com.android.server.wm.shell.TransitionTraceProto.REAL_TO_ELAPSED_TIME_OFFSET_NANOS;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.os.SystemClock;
-import android.os.Trace;
-import android.util.Log;
-import android.util.proto.ProtoOutputStream;
 
-import com.android.internal.util.TraceBuffer;
-import com.android.server.wm.Transition.ChangeInfo;
-
-import java.io.File;
-import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.concurrent.TimeUnit;
 
-/**
- * Helper class to collect and dump transition traces.
- */
-public class TransitionTracer {
+interface TransitionTracer {
+    void logSentTransition(Transition transition, ArrayList<Transition.ChangeInfo> targets);
+    void logFinishedTransition(Transition transition);
+    void logAbortedTransition(Transition transition);
+    void logRemovingStartingWindow(@NonNull StartingData startingData);
 
-    private static final String LOG_TAG = "TransitionTracer";
-
-    private static final int ALWAYS_ON_TRACING_CAPACITY = 15 * 1024; // 15 KB
-    private static final int ACTIVE_TRACING_BUFFER_CAPACITY = 5000 * 1024; // 5 MB
-
-    // This will be the size the proto output streams are initialized to.
-    // Ideally this should fit most or all the proto objects we will create and be no bigger than
-    // that to ensure to don't use excessive amounts of memory.
-    private static final int CHUNK_SIZE = 64;
-
-    static final String WINSCOPE_EXT = ".winscope";
-    private static final String TRACE_FILE =
-            "/data/misc/wmtrace/wm_transition_trace" + WINSCOPE_EXT;
-    private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
-
-    private final TraceBuffer mTraceBuffer = new TraceBuffer(ALWAYS_ON_TRACING_CAPACITY);
-
-    private final Object mEnabledLock = new Object();
-    private volatile boolean mActiveTracingEnabled = false;
-
-    /**
-     * Records key information about a transition that has been sent to Shell to be played.
-     * More information will be appended to the same proto object once the transition is finished or
-     * aborted.
-     * Transition information won't be added to the trace buffer until
-     * {@link #logFinishedTransition} or {@link #logAbortedTransition} is called for this
-     * transition.
-     *
-     * @param transition The transition that has been sent to Shell.
-     * @param targets Information about the target windows of the transition.
-     */
-    public void logSentTransition(Transition transition, ArrayList<ChangeInfo> targets) {
-        try {
-            final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
-            final long protoToken = outputStream
-                    .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
-            outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
-            outputStream.write(com.android.server.wm.shell.Transition.CREATE_TIME_NS,
-                    transition.mLogger.mCreateTimeNs);
-            outputStream.write(com.android.server.wm.shell.Transition.SEND_TIME_NS,
-                    transition.mLogger.mSendTimeNs);
-            outputStream.write(com.android.server.wm.shell.Transition.START_TRANSACTION_ID,
-                    transition.getStartTransaction().getId());
-            outputStream.write(com.android.server.wm.shell.Transition.FINISH_TRANSACTION_ID,
-                    transition.getFinishTransaction().getId());
-            dumpTransitionTargetsToProto(outputStream, transition, targets);
-            outputStream.end(protoToken);
-
-            mTraceBuffer.add(outputStream);
-        } catch (Exception e) {
-            // Don't let any errors in the tracing cause the transition to fail
-            Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
-        }
-    }
-
-    /**
-     * Completes the information dumped in {@link #logSentTransition} for a transition
-     * that has finished or aborted, and add the proto object to the trace buffer.
-     *
-     * @param transition The transition that has finished.
-     */
-    public void logFinishedTransition(Transition transition) {
-        try {
-            final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
-            final long protoToken = outputStream
-                    .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
-            outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
-            outputStream.write(com.android.server.wm.shell.Transition.FINISH_TIME_NS,
-                    transition.mLogger.mFinishTimeNs);
-            outputStream.end(protoToken);
-
-            mTraceBuffer.add(outputStream);
-        } catch (Exception e) {
-            // Don't let any errors in the tracing cause the transition to fail
-            Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
-        }
-    }
-
-    /**
-     * Same as {@link #logFinishedTransition} but don't add the transition to the trace buffer
-     * unless actively tracing.
-     *
-     * @param transition The transition that has been aborted
-     */
-    public void logAbortedTransition(Transition transition) {
-        try {
-            final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
-            final long protoToken = outputStream
-                    .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
-            outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId());
-            outputStream.write(com.android.server.wm.shell.Transition.ABORT_TIME_NS,
-                    transition.mLogger.mAbortTimeNs);
-            outputStream.end(protoToken);
-
-            mTraceBuffer.add(outputStream);
-        } catch (Exception e) {
-            // Don't let any errors in the tracing cause the transition to fail
-            Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
-        }
-    }
-
-    void logRemovingStartingWindow(@NonNull StartingData startingData) {
-        if (startingData.mTransitionId == 0) {
-            return;
-        }
-        try {
-            final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE);
-            final long protoToken = outputStream
-                    .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS);
-            outputStream.write(com.android.server.wm.shell.Transition.ID,
-                    startingData.mTransitionId);
-            outputStream.write(
-                    com.android.server.wm.shell.Transition.STARTING_WINDOW_REMOVE_TIME_NS,
-                    SystemClock.elapsedRealtimeNanos());
-            outputStream.end(protoToken);
-
-            mTraceBuffer.add(outputStream);
-        } catch (Exception e) {
-            Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e);
-        }
-    }
-
-    private void dumpTransitionTargetsToProto(ProtoOutputStream outputStream,
-            Transition transition, ArrayList<ChangeInfo> targets) {
-        Trace.beginSection("TransitionTracer#dumpTransitionTargetsToProto");
-        if (mActiveTracingEnabled) {
-            outputStream.write(com.android.server.wm.shell.Transition.ID,
-                    transition.getSyncId());
-        }
-
-        outputStream.write(com.android.server.wm.shell.Transition.TYPE, transition.mType);
-        outputStream.write(com.android.server.wm.shell.Transition.FLAGS, transition.getFlags());
-
-        for (int i = 0; i < targets.size(); ++i) {
-            final long changeToken = outputStream
-                    .start(com.android.server.wm.shell.Transition.TARGETS);
-
-            final Transition.ChangeInfo target = targets.get(i);
-
-            final int layerId;
-            if (target.mContainer.mSurfaceControl.isValid()) {
-                layerId = target.mContainer.mSurfaceControl.getLayerId();
-            } else {
-                layerId = -1;
-            }
-
-            outputStream.write(com.android.server.wm.shell.Target.MODE, target.mReadyMode);
-            outputStream.write(com.android.server.wm.shell.Target.FLAGS, target.mReadyFlags);
-            outputStream.write(com.android.server.wm.shell.Target.LAYER_ID, layerId);
-
-            if (mActiveTracingEnabled) {
-                // What we use in the WM trace
-                final int windowId = System.identityHashCode(target.mContainer);
-                outputStream.write(com.android.server.wm.shell.Target.WINDOW_ID, windowId);
-            }
-
-            outputStream.end(changeToken);
-        }
-
-        Trace.endSection();
-    }
-
-    /**
-     * Starts collecting transitions for the trace.
-     * If called while a trace is already running, this will reset the trace.
-     */
-    public void startTrace(@Nullable PrintWriter pw) {
-        if (IS_USER) {
-            LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
-            return;
-        }
-        Trace.beginSection("TransitionTracer#startTrace");
-        LogAndPrintln.i(pw, "Starting shell transition trace.");
-        synchronized (mEnabledLock) {
-            mActiveTracingEnabled = true;
-            mTraceBuffer.resetBuffer();
-            mTraceBuffer.setCapacity(ACTIVE_TRACING_BUFFER_CAPACITY);
-        }
-        Trace.endSection();
-    }
-
-    /**
-     * Stops collecting the transition trace and dump to trace to file.
-     *
-     * Dumps the trace to @link{TRACE_FILE}.
-     */
-    public void stopTrace(@Nullable PrintWriter pw) {
-        stopTrace(pw, new File(TRACE_FILE));
-    }
-
-    /**
-     * Stops collecting the transition trace and dump to trace to file.
-     * @param outputFile The file to dump the transition trace to.
-     */
-    public void stopTrace(@Nullable PrintWriter pw, File outputFile) {
-        if (IS_USER) {
-            LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
-            return;
-        }
-        Trace.beginSection("TransitionTracer#stopTrace");
-        LogAndPrintln.i(pw, "Stopping shell transition trace.");
-        synchronized (mEnabledLock) {
-            mActiveTracingEnabled = false;
-            writeTraceToFileLocked(pw, outputFile);
-            mTraceBuffer.resetBuffer();
-            mTraceBuffer.setCapacity(ALWAYS_ON_TRACING_CAPACITY);
-        }
-        Trace.endSection();
-    }
-
-    /**
-     * Being called while taking a bugreport so that tracing files can be included in the bugreport.
-     *
-     * @param pw Print writer
-     */
-    public void saveForBugreport(@Nullable PrintWriter pw) {
-        if (IS_USER) {
-            LogAndPrintln.e(pw, "Tracing is not supported on user builds.");
-            return;
-        }
-        Trace.beginSection("TransitionTracer#saveForBugreport");
-        synchronized (mEnabledLock) {
-            final File outputFile = new File(TRACE_FILE);
-            writeTraceToFileLocked(pw, outputFile);
-        }
-        Trace.endSection();
-    }
-
-    boolean isActiveTracingEnabled() {
-        return mActiveTracingEnabled;
-    }
-
-    private void writeTraceToFileLocked(@Nullable PrintWriter pw, File file) {
-        Trace.beginSection("TransitionTracer#writeTraceToFileLocked");
-        try {
-            ProtoOutputStream proto = new ProtoOutputStream(CHUNK_SIZE);
-            proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE);
-            long timeOffsetNs =
-                    TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis())
-                            - SystemClock.elapsedRealtimeNanos();
-            proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs);
-            int pid = android.os.Process.myPid();
-            LogAndPrintln.i(pw, "Writing file to " + file.getAbsolutePath()
-                    + " from process " + pid);
-            mTraceBuffer.writeTraceToFile(file, proto);
-        } catch (IOException e) {
-            LogAndPrintln.e(pw, "Unable to write buffer to file", e);
-        }
-        Trace.endSection();
-    }
-
-    private static class LogAndPrintln {
-        private static void i(@Nullable PrintWriter pw, String msg) {
-            Log.i(LOG_TAG, msg);
-            if (pw != null) {
-                pw.println(msg);
-                pw.flush();
-            }
-        }
-
-        private static void e(@Nullable PrintWriter pw, String msg) {
-            Log.e(LOG_TAG, msg);
-            if (pw != null) {
-                pw.println("ERROR: " + msg);
-                pw.flush();
-            }
-        }
-
-        private static void e(@Nullable PrintWriter pw, String msg, @NonNull Exception e) {
-            Log.e(LOG_TAG, msg, e);
-            if (pw != null) {
-                pw.println("ERROR: " + msg + " ::\n " + e);
-                pw.flush();
-            }
-        }
-    }
+    void startTrace(@Nullable PrintWriter pw);
+    void stopTrace(@Nullable PrintWriter pw);
+    boolean isTracing();
+    void saveForBugreport(@Nullable PrintWriter pw);
 }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 9544835..b38dc00 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1213,7 +1213,12 @@
 
         mWindowTracing = WindowTracing.createDefaultAndStartLooper(this,
                 Choreographer.getInstance());
-        mTransitionTracer = new TransitionTracer();
+
+        if (android.tracing.Flags.perfettoTransitionTracing()) {
+            mTransitionTracer = new PerfettoTransitionTracer();
+        } else {
+            mTransitionTracer = new LegacyTransitionTracer();
+        }
 
         LocalServices.addService(WindowManagerPolicy.class, mPolicy);
 
@@ -6087,7 +6092,7 @@
 
     @Override
     public boolean isTransitionTraceEnabled() {
-        return mTransitionTracer.isActiveTracingEnabled();
+        return mTransitionTracer.isTracing();
     }
 
     @Override
diff --git a/services/profcollect/Android.bp b/services/profcollect/Android.bp
index 2040bb6..fe431f5 100644
--- a/services/profcollect/Android.bp
+++ b/services/profcollect/Android.bp
@@ -22,24 +22,25 @@
 }
 
 filegroup {
-  name: "services.profcollect-javasources",
-  srcs: ["src/**/*.java"],
-  path: "src",
-  visibility: ["//frameworks/base/services"],
+    name: "services.profcollect-javasources",
+    srcs: ["src/**/*.java"],
+    path: "src",
+    visibility: ["//frameworks/base/services"],
 }
 
 filegroup {
-  name: "services.profcollect-sources",
-  srcs: [
-    ":services.profcollect-javasources",
-    ":profcollectd_aidl",
-  ],
-  visibility: ["//frameworks/base/services:__subpackages__"],
+    name: "services.profcollect-sources",
+    srcs: [
+        ":services.profcollect-javasources",
+        ":profcollectd_aidl",
+    ],
+    visibility: ["//frameworks/base/services:__subpackages__"],
 }
 
 java_library_static {
-  name: "services.profcollect",
-  defaults: ["platform_service_defaults"],
-  srcs: [":services.profcollect-sources"],
-  libs: ["services.core"],
+    name: "services.profcollect",
+    defaults: ["platform_service_defaults"],
+    srcs: [":services.profcollect-sources"],
+    static_libs: ["services.core"],
+    libs: ["service-art.stubs.system_server"],
 }
diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
index 4007672..582b712 100644
--- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
+++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
@@ -41,12 +41,15 @@
 import com.android.internal.R;
 import com.android.internal.os.BackgroundThread;
 import com.android.server.IoThread;
+import com.android.server.LocalManagerRegistry;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
+import com.android.server.art.ArtManagerLocal;
 import com.android.server.wm.ActivityMetricsLaunchObserver;
 import com.android.server.wm.ActivityMetricsLaunchObserverRegistry;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
+import java.util.concurrent.ForkJoinPool;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
 
@@ -261,6 +264,7 @@
         BackgroundThread.get().getThreadHandler().post(
                 () -> {
                     registerAppLaunchObserver();
+                    registerDex2oatObserver();
                     registerOTAObserver();
                 });
     }
@@ -304,6 +308,44 @@
         }
     }
 
+    private void registerDex2oatObserver() {
+        ArtManagerLocal aml = LocalManagerRegistry.getManager(ArtManagerLocal.class);
+        if (aml == null) {
+            Log.w(LOG_TAG, "Couldn't get ArtManagerLocal");
+            return;
+        }
+        aml.setBatchDexoptStartCallback(ForkJoinPool.commonPool(),
+                (snapshot, reason, defaultPackages, builder, passedSignal) -> {
+                    traceOnDex2oatStart();
+                });
+    }
+
+    private void traceOnDex2oatStart() {
+        if (mIProfcollect == null) {
+            return;
+        }
+        // Sample for a fraction of dex2oat runs.
+        final int traceFrequency =
+            DeviceConfig.getInt(DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT,
+                "dex2oat_trace_freq", 10);
+        int randomNum = ThreadLocalRandom.current().nextInt(100);
+        if (randomNum < traceFrequency) {
+            if (DEBUG) {
+                Log.d(LOG_TAG, "Tracing on dex2oat event");
+            }
+            BackgroundThread.get().getThreadHandler().post(() -> {
+                try {
+                    // Dex2oat could take a while before it starts. Add a short delay before start
+                    // tracing.
+                    Thread.sleep(1000);
+                    mIProfcollect.trace_once("dex2oat");
+                } catch (RemoteException | InterruptedException e) {
+                    Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage());
+                }
+            });
+        }
+    }
+
     private void registerOTAObserver() {
         UpdateEngine updateEngine = new UpdateEngine();
         updateEngine.bind(new UpdateEngineCallback() {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt
new file mode 100644
index 0000000..638924e
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.mode
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.hardware.display.BrightnessInfo
+import android.view.Display
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SmallTest
+import com.android.server.display.DisplayDeviceConfig
+import com.android.server.display.feature.DisplayManagerFlags
+import com.android.server.testutils.TestHandler
+import com.google.common.truth.Truth.assertThat
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.junit.MockitoJUnit
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(TestParameterInjector::class)
+class BrightnessObserverTest {
+
+    @get:Rule
+    val mockitoRule = MockitoJUnit.rule()
+
+    private lateinit var spyContext: Context
+    private val mockInjector = mock<DisplayModeDirector.Injector>()
+    private val mockFlags = mock<DisplayManagerFlags>()
+    private val mockDeviceConfig = mock<DisplayDeviceConfig>()
+
+    private val testHandler = TestHandler(null)
+
+    @Before
+    fun setUp() {
+        spyContext = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
+    }
+
+    @Test
+    fun testLowLightBlockingZoneVotes(@TestParameter testCase: LowLightTestCase) {
+        setUpLowBrightnessZone()
+        whenever(mockFlags.isVsyncLowLightVoteEnabled).thenReturn(testCase.vsyncLowLightVoteEnabled)
+        val displayModeDirector = DisplayModeDirector(
+                spyContext, testHandler, mockInjector, mockFlags)
+        val brightnessObserver = displayModeDirector.BrightnessObserver(
+                spyContext, testHandler, mockInjector, testCase.vrrSupported, mockFlags)
+
+        brightnessObserver.onRefreshRateSettingChangedLocked(0.0f, 120.0f)
+        brightnessObserver.updateBlockingZoneThresholds(mockDeviceConfig, false)
+        brightnessObserver.onDeviceConfigRefreshRateInLowZoneChanged(60)
+
+        brightnessObserver.onDisplayChanged(Display.DEFAULT_DISPLAY)
+
+        assertThat(displayModeDirector.getVote(VotesStorage.GLOBAL_ID,
+                Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH)).isEqualTo(testCase.expectedVote)
+    }
+
+    private fun setUpLowBrightnessZone() {
+        whenever(mockInjector.getBrightnessInfo(Display.DEFAULT_DISPLAY)).thenReturn(
+                BrightnessInfo(/* brightness = */ 0.05f, /* adjustedBrightness = */ 0.05f,
+                        /* brightnessMinimum = */ 0.0f, /* brightnessMaximum = */ 1.0f,
+                        BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF,
+                        /* highBrightnessTransitionPoint = */ 1.0f,
+                        BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE))
+        whenever(mockDeviceConfig.highDisplayBrightnessThresholds).thenReturn(floatArrayOf())
+        whenever(mockDeviceConfig.highAmbientBrightnessThresholds).thenReturn(floatArrayOf())
+        whenever(mockDeviceConfig.lowDisplayBrightnessThresholds).thenReturn(floatArrayOf(0.1f))
+        whenever(mockDeviceConfig.lowAmbientBrightnessThresholds).thenReturn(floatArrayOf(10f))
+    }
+
+    enum class LowLightTestCase(
+            val vrrSupported: Boolean,
+            val vsyncLowLightVoteEnabled: Boolean,
+            internal val expectedVote: Vote
+    ) {
+        ALL_ENABLED(true, true, CombinedVote(
+                listOf(DisableRefreshRateSwitchingVote(true),
+                        SupportedModesVote(
+                                listOf(SupportedModesVote.SupportedMode(60f, 60f),
+                                        SupportedModesVote.SupportedMode(120f, 120f)))))),
+        VRR_NOT_SUPPORTED(false, true, DisableRefreshRateSwitchingVote(true)),
+        VSYNC_VOTE_DISABLED(true, false, DisableRefreshRateSwitchingVote(true))
+    }
+}
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActiveServicesTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActiveServicesTest.java
index e2c338a..7e1dc08 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ActiveServicesTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ActiveServicesTest.java
@@ -202,7 +202,7 @@
         final ServiceInfo regularService = new ServiceInfo();
         regularService.processName = "com.foo";
         String processName = ActiveServices.getProcessNameForService(regularService, null, null,
-                null, false, false);
+                null, false, false, false);
         assertEquals("com.foo", processName);
 
         // Isolated service
@@ -211,29 +211,90 @@
         isolatedService.flags = ServiceInfo.FLAG_ISOLATED_PROCESS;
         final ComponentName component = new ComponentName("com.foo", "barService");
         processName = ActiveServices.getProcessNameForService(isolatedService, component,
-                null, null, false, false);
+                null, null, false, false, false);
         assertEquals("com.foo:barService", processName);
 
+        // Isolated Service in package private process.
+        final ServiceInfo isolatedService1 = new ServiceInfo();
+        isolatedService1.processName = "com.foo:trusted_isolated";
+        isolatedService1.flags = ServiceInfo.FLAG_ISOLATED_PROCESS;
+        final ComponentName componentName = new ComponentName("com.foo", "barService");
+        processName = ActiveServices.getProcessNameForService(isolatedService1, componentName,
+                null, null, false, false, false);
+        assertEquals("com.foo:trusted_isolated:barService", processName);
+
+        // Isolated service in package-private shared process (main process)
+        final ServiceInfo isolatedPackageSharedService = new ServiceInfo();
+        final ComponentName componentName1 = new ComponentName("com.foo", "barService");
+        isolatedPackageSharedService.processName = "com.foo";
+        isolatedPackageSharedService.applicationInfo = new ApplicationInfo();
+        isolatedPackageSharedService.applicationInfo.processName = "com.foo";
+        isolatedPackageSharedService.flags = ServiceInfo.FLAG_ISOLATED_PROCESS;
+        String packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService(
+                isolatedPackageSharedService, componentName1, null, null, false, false, true);
+        assertEquals("com.foo:barService", packageSharedIsolatedProcessName);
+
+        // Isolated service in package-private shared process
+        final ServiceInfo isolatedPackageSharedService1 = new ServiceInfo(
+                isolatedPackageSharedService);
+        isolatedPackageSharedService1.processName = "com.foo:trusted_isolated";
+        isolatedPackageSharedService1.applicationInfo = new ApplicationInfo();
+        isolatedPackageSharedService1.applicationInfo.processName = "com.foo";
+        isolatedPackageSharedService1.flags = ServiceInfo.FLAG_ISOLATED_PROCESS;
+        packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService(
+                isolatedPackageSharedService1, componentName1, null, null, false, false, true);
+        assertEquals("com.foo:trusted_isolated", packageSharedIsolatedProcessName);
+
+
+        // Bind another one in the same isolated process
+        final ServiceInfo isolatedPackageSharedService2 = new ServiceInfo(
+                isolatedPackageSharedService1);
+        packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService(
+                isolatedPackageSharedService2, componentName1, null, null, false, false, true);
+        assertEquals("com.foo:trusted_isolated", packageSharedIsolatedProcessName);
+
+        // Simulate another app trying to do the bind.
+        final ServiceInfo isolatedPackageSharedService3 = new ServiceInfo(
+                isolatedPackageSharedService1);
+        final String auxCallingPackage = "com.bar";
+        packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService(
+                isolatedPackageSharedService3, componentName1, auxCallingPackage, null,
+                false, false, true);
+        assertEquals("com.foo:trusted_isolated", packageSharedIsolatedProcessName);
+
+        // Simulate another app owning the service
+        final ServiceInfo isolatedOtherPackageSharedService = new ServiceInfo(
+                isolatedPackageSharedService1);
+        final ComponentName componentName2 = new ComponentName("com.bar", "barService");
+        isolatedOtherPackageSharedService.processName = "com.bar:isolated";
+        isolatedPackageSharedService.applicationInfo.processName = "com.bar";
+        final String mainCallingPackage = "com.foo";
+        packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService(
+                isolatedOtherPackageSharedService, componentName2, mainCallingPackage,
+                null, false, false, true);
+        assertEquals("com.bar:isolated", packageSharedIsolatedProcessName);
+
         // Isolated service in shared isolated process
         final ServiceInfo isolatedServiceShared1 = new ServiceInfo();
         isolatedServiceShared1.flags = ServiceInfo.FLAG_ISOLATED_PROCESS;
         final String instanceName = "pool";
         final String callingPackage = "com.foo";
         final String sharedIsolatedProcessName1 = ActiveServices.getProcessNameForService(
-                isolatedServiceShared1, null, callingPackage, instanceName, false, true);
+                isolatedServiceShared1, null, callingPackage, instanceName, false, true, false);
         assertEquals("com.foo:ishared:pool", sharedIsolatedProcessName1);
 
         // Bind another one in the same isolated process
         final ServiceInfo isolatedServiceShared2 = new ServiceInfo(isolatedServiceShared1);
         final String sharedIsolatedProcessName2 = ActiveServices.getProcessNameForService(
-                isolatedServiceShared2, null, callingPackage, instanceName, false, true);
+                isolatedServiceShared2, null, callingPackage, instanceName, false, true, false);
         assertEquals(sharedIsolatedProcessName1, sharedIsolatedProcessName2);
 
         // Simulate another app trying to do the bind
         final ServiceInfo isolatedServiceShared3 = new ServiceInfo(isolatedServiceShared1);
         final String otherCallingPackage = "com.bar";
         final String sharedIsolatedProcessName3 = ActiveServices.getProcessNameForService(
-                isolatedServiceShared3, null, otherCallingPackage, instanceName, false, true);
+                isolatedServiceShared3, null, otherCallingPackage, instanceName, false, true,
+                false);
         Assert.assertNotEquals(sharedIsolatedProcessName2, sharedIsolatedProcessName3);
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 5e7deef..d71844b 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -1971,7 +1971,7 @@
                         mRunningAppsChangedCallback,
                         params,
                         new DisplayManagerGlobal(mIDisplayManager),
-                        new VirtualCameraController());
+                        new VirtualCameraController(DEVICE_POLICY_DEFAULT));
         mVdms.addVirtualDevice(virtualDeviceImpl);
         assertThat(virtualDeviceImpl.getAssociationId()).isEqualTo(mAssociationInfo.getId());
         assertThat(virtualDeviceImpl.getPersistentDeviceId())
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
index 9b28b81..3e4f1df 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
@@ -16,27 +16,35 @@
 
 package com.android.server.companion.virtual.camera;
 
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.camera.VirtualCameraConfig.SENSOR_ORIENTATION_0;
+import static android.companion.virtual.camera.VirtualCameraConfig.SENSOR_ORIENTATION_90;
+import static android.graphics.ImageFormat.YUV_420_888;
+import static android.graphics.PixelFormat.RGBA_8888;
+import static android.hardware.camera2.CameraMetadata.LENS_FACING_BACK;
+import static android.hardware.camera2.CameraMetadata.LENS_FACING_FRONT;
+
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.annotation.NonNull;
 import android.companion.virtual.camera.VirtualCameraCallback;
 import android.companion.virtual.camera.VirtualCameraConfig;
-import android.companion.virtual.camera.VirtualCameraStreamConfig;
 import android.companion.virtualcamera.IVirtualCameraService;
 import android.companion.virtualcamera.VirtualCameraConfiguration;
-import android.graphics.ImageFormat;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.Looper;
 import android.platform.test.annotations.Presubmit;
-import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
-import android.view.Surface;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
 
 import org.junit.After;
 import org.junit.Before;
@@ -49,21 +57,30 @@
 import java.util.List;
 
 @Presubmit
-@RunWith(AndroidTestingRunner.class)
+@RunWith(JUnitParamsRunner.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class VirtualCameraControllerTest {
 
     private static final String CAMERA_NAME_1 = "Virtual camera 1";
     private static final int CAMERA_WIDTH_1 = 100;
     private static final int CAMERA_HEIGHT_1 = 200;
+    private static final int CAMERA_FORMAT_1 = YUV_420_888;
+    private static final int CAMERA_MAX_FPS_1 = 30;
+    private static final int CAMERA_SENSOR_ORIENTATION_1 = SENSOR_ORIENTATION_0;
+    private static final int CAMERA_LENS_FACING_1 = LENS_FACING_BACK;
 
     private static final String CAMERA_NAME_2 = "Virtual camera 2";
     private static final int CAMERA_WIDTH_2 = 400;
     private static final int CAMERA_HEIGHT_2 = 600;
-    private static final int CAMERA_FORMAT = ImageFormat.YUV_420_888;
+    private static final int CAMERA_FORMAT_2 = RGBA_8888;
+    private static final int CAMERA_MAX_FPS_2 = 60;
+    private static final int CAMERA_SENSOR_ORIENTATION_2 = SENSOR_ORIENTATION_90;
+    private static final int CAMERA_LENS_FACING_2 = LENS_FACING_FRONT;
 
     @Mock
     private IVirtualCameraService mVirtualCameraServiceMock;
+    @Mock
+    private VirtualCameraCallback mVirtualCameraCallbackMock;
 
     private VirtualCameraController mVirtualCameraController;
     private final HandlerExecutor mCallbackHandler =
@@ -72,7 +89,8 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mVirtualCameraController = new VirtualCameraController(mVirtualCameraServiceMock);
+        mVirtualCameraController = new VirtualCameraController(mVirtualCameraServiceMock,
+                DEVICE_POLICY_CUSTOM);
         when(mVirtualCameraServiceMock.registerCamera(any(), any())).thenReturn(true);
     }
 
@@ -81,10 +99,12 @@
         mVirtualCameraController.close();
     }
 
+    @Parameters(method = "getAllLensFacingDirections")
     @Test
-    public void registerCamera_registersCamera() throws Exception {
+    public void registerCamera_registersCamera(int lensFacing) throws Exception {
         mVirtualCameraController.registerCamera(createVirtualCameraConfig(
-                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_NAME_1));
+                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_NAME_1,
+                CAMERA_SENSOR_ORIENTATION_1, lensFacing));
 
         ArgumentCaptor<VirtualCameraConfiguration> configurationCaptor =
                 ArgumentCaptor.forClass(VirtualCameraConfiguration.class);
@@ -92,13 +112,15 @@
         VirtualCameraConfiguration virtualCameraConfiguration = configurationCaptor.getValue();
         assertThat(virtualCameraConfiguration.supportedStreamConfigs.length).isEqualTo(1);
         assertVirtualCameraConfiguration(virtualCameraConfiguration, CAMERA_WIDTH_1,
-                CAMERA_HEIGHT_1, CAMERA_FORMAT);
+                CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_SENSOR_ORIENTATION_1,
+                lensFacing);
     }
 
     @Test
     public void unregisterCamera_unregistersCamera() throws Exception {
         VirtualCameraConfig config = createVirtualCameraConfig(
-                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_NAME_1);
+                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_NAME_1,
+                CAMERA_SENSOR_ORIENTATION_1, CAMERA_LENS_FACING_1);
         mVirtualCameraController.registerCamera(config);
 
         mVirtualCameraController.unregisterCamera(config);
@@ -109,9 +131,11 @@
     @Test
     public void close_unregistersAllCameras() throws Exception {
         mVirtualCameraController.registerCamera(createVirtualCameraConfig(
-                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_NAME_1));
+                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_NAME_1,
+                CAMERA_SENSOR_ORIENTATION_1, CAMERA_LENS_FACING_1));
         mVirtualCameraController.registerCamera(createVirtualCameraConfig(
-                CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT, CAMERA_NAME_2));
+                CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT_2, CAMERA_MAX_FPS_2, CAMERA_NAME_2,
+                CAMERA_SENSOR_ORIENTATION_2, CAMERA_LENS_FACING_2));
 
         mVirtualCameraController.close();
 
@@ -123,38 +147,66 @@
                 configurationCaptor.getAllValues();
         assertThat(virtualCameraConfigurations).hasSize(2);
         assertVirtualCameraConfiguration(virtualCameraConfigurations.get(0), CAMERA_WIDTH_1,
-                CAMERA_HEIGHT_1, CAMERA_FORMAT);
+                CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_SENSOR_ORIENTATION_1,
+                CAMERA_LENS_FACING_1);
         assertVirtualCameraConfiguration(virtualCameraConfigurations.get(1), CAMERA_WIDTH_2,
-                CAMERA_HEIGHT_2, CAMERA_FORMAT);
+                CAMERA_HEIGHT_2, CAMERA_FORMAT_2, CAMERA_MAX_FPS_2, CAMERA_SENSOR_ORIENTATION_2,
+                CAMERA_LENS_FACING_2);
+    }
+
+    @Parameters(method = "getAllLensFacingDirections")
+    @Test
+    public void registerMultipleSameLensFacingCameras_withCustomCameraPolicy_throwsException(
+            int lensFacing) {
+        mVirtualCameraController.registerCamera(createVirtualCameraConfig(
+                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_NAME_1,
+                CAMERA_SENSOR_ORIENTATION_1, lensFacing));
+        assertThrows(IllegalArgumentException.class,
+                () -> mVirtualCameraController.registerCamera(createVirtualCameraConfig(
+                        CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT_2, CAMERA_MAX_FPS_2,
+                        CAMERA_NAME_2, CAMERA_SENSOR_ORIENTATION_2, lensFacing)));
+    }
+
+    @Parameters(method = "getAllLensFacingDirections")
+    @Test
+    public void registerCamera_withDefaultCameraPolicy_throwsException(int lensFacing) {
+        mVirtualCameraController.close();
+        mVirtualCameraController = new VirtualCameraController(
+                mVirtualCameraServiceMock, DEVICE_POLICY_DEFAULT);
+
+        assertThrows(IllegalArgumentException.class,
+                () -> mVirtualCameraController.registerCamera(createVirtualCameraConfig(
+                        CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1,
+                        CAMERA_NAME_1, CAMERA_SENSOR_ORIENTATION_1, lensFacing)));
     }
 
     private VirtualCameraConfig createVirtualCameraConfig(
-            int width, int height, int format, String displayName) {
+            int width, int height, int format, int maximumFramesPerSecond,
+            String name, int sensorOrientation, int lensFacing) {
         return new VirtualCameraConfig.Builder()
-                .addStreamConfig(width, height, format)
-                .setName(displayName)
-                .setVirtualCameraCallback(mCallbackHandler, createNoOpCallback())
+                .addStreamConfig(width, height, format, maximumFramesPerSecond)
+                .setName(name)
+                .setVirtualCameraCallback(mCallbackHandler, mVirtualCameraCallbackMock)
+                .setSensorOrientation(sensorOrientation)
+                .setLensFacing(lensFacing)
                 .build();
     }
 
     private static void assertVirtualCameraConfiguration(
-            VirtualCameraConfiguration configuration, int width, int height, int format) {
+            VirtualCameraConfiguration configuration, int width, int height, int format,
+            int maxFps, int sensorOrientation, int lensFacing) {
         assertThat(configuration.supportedStreamConfigs[0].width).isEqualTo(width);
         assertThat(configuration.supportedStreamConfigs[0].height).isEqualTo(height);
         assertThat(configuration.supportedStreamConfigs[0].pixelFormat).isEqualTo(format);
+        assertThat(configuration.supportedStreamConfigs[0].maxFps).isEqualTo(maxFps);
+        assertThat(configuration.sensorOrientation).isEqualTo(sensorOrientation);
+        assertThat(configuration.lensFacing).isEqualTo(lensFacing);
     }
 
-    private static VirtualCameraCallback createNoOpCallback() {
-        return new VirtualCameraCallback() {
-
-            @Override
-            public void onStreamConfigured(
-                    int streamId,
-                    @NonNull Surface surface,
-                    @NonNull VirtualCameraStreamConfig streamConfig) {}
-
-            @Override
-            public void onStreamClosed(int streamId) {}
+    private static Integer[] getAllLensFacingDirections() {
+        return new Integer[] {
+                LENS_FACING_BACK,
+                LENS_FACING_FRONT
         };
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java
index d9a38eb..206c111 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java
@@ -35,19 +35,20 @@
 
     private static final int VGA_WIDTH = 640;
     private static final int VGA_HEIGHT = 480;
+    private static final int MAX_FPS_1 = 30;
 
     private static final int QVGA_WIDTH = 320;
     private static final int QVGA_HEIGHT = 240;
+    private static final int MAX_FPS_2 = 60;
 
     @Test
     public void testEquals() {
         VirtualCameraStreamConfig vgaYuvStreamConfig = new VirtualCameraStreamConfig(VGA_WIDTH,
-                VGA_HEIGHT,
-                ImageFormat.YUV_420_888);
+                VGA_HEIGHT, ImageFormat.YUV_420_888, MAX_FPS_1);
         VirtualCameraStreamConfig qvgaYuvStreamConfig = new VirtualCameraStreamConfig(QVGA_WIDTH,
-                QVGA_HEIGHT, ImageFormat.YUV_420_888);
+                QVGA_HEIGHT, ImageFormat.YUV_420_888, MAX_FPS_2);
         VirtualCameraStreamConfig vgaRgbaStreamConfig = new VirtualCameraStreamConfig(VGA_WIDTH,
-                VGA_HEIGHT, PixelFormat.RGBA_8888);
+                VGA_HEIGHT, PixelFormat.RGBA_8888, MAX_FPS_1);
 
         new EqualsTester()
                 .addEqualityGroup(vgaYuvStreamConfig, reparcel(vgaYuvStreamConfig))
@@ -66,6 +67,4 @@
             parcel.recycle();
         }
     }
-
-
 }
diff --git a/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java
index 769ec5f..3218586 100644
--- a/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java
@@ -345,15 +345,18 @@
                 intent, UID_PRIMARY_CAMERA, PKG_SOCIAL, USER_PRIMARY), service);
 
         // Verify that everything is good with the world
-        assertTrue(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ));
+        assertTrue(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ,
+                /* isFullAccessForContentUri */ false));
 
         // Finish activity; service should hold permission
         activity.removeUriPermissions();
-        assertTrue(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ));
+        assertTrue(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ,
+                /* isFullAccessForContentUri */ false));
 
         // And finishing service should wrap things up
         service.removeUriPermissions();
-        assertFalse(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ));
+        assertFalse(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ,
+                /* isFullAccessForContentUri */ false));
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java b/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java
index 2d3c4bb..2ea5dc4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java
@@ -157,8 +157,11 @@
         DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
                 KEY_SPLASH_SCREEN_EXCEPTION_LIST, commaSeparatedList, false);
         try {
-            assertTrue("Timed out waiting for DeviceConfig to be updated.",
-                    latch.await(5, TimeUnit.SECONDS));
+            if (!latch.await(1, TimeUnit.SECONDS)) {
+                Log.w(getClass().getSimpleName(),
+                        "Timed out waiting for DeviceConfig to be updated. Force update.");
+                mList.updateDeviceConfig(commaSeparatedList);
+            }
         } catch (InterruptedException e) {
             Assert.fail(e.getMessage());
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java
index 339162a..dfea2fc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java
@@ -44,7 +44,6 @@
 import android.view.animation.Animation;
 import android.view.animation.TranslateAnimation;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 
 import com.android.server.AnimationThread;
@@ -147,9 +146,10 @@
         assertFinishCallbackNotCalled();
     }
 
-    @FlakyTest(bugId = 71719744)
     @Test
     public void testCancel_sneakyCancelBeforeUpdate() throws Exception {
+        final CountDownLatch animationCancelled = new CountDownLatch(1);
+
         mSurfaceAnimationRunner = new SurfaceAnimationRunner(null, () -> new ValueAnimator() {
             {
                 setFloatValues(0f, 1f);
@@ -162,6 +162,7 @@
                     // interleaving of multiple threads. Muahahaha
                     if (animation.getCurrentPlayTime() > 0) {
                         mSurfaceAnimationRunner.onAnimationCancelled(mMockSurface);
+                        animationCancelled.countDown();
                     }
                     listener.onAnimationUpdate(animation);
                 });
@@ -170,11 +171,7 @@
         when(mMockAnimationSpec.getDuration()).thenReturn(200L);
         mSurfaceAnimationRunner.startAnimation(mMockAnimationSpec, mMockSurface, mMockTransaction,
                 this::finishedCallback);
-
-        // We need to wait for two frames: The first frame starts the animation, the second frame
-        // actually cancels the animation.
-        waitUntilNextFrame();
-        waitUntilNextFrame();
+        assertTrue(animationCancelled.await(1, SECONDS));
         assertTrue(mSurfaceAnimationRunner.mRunningAnimations.isEmpty());
         verify(mMockAnimationSpec, atLeastOnce()).apply(any(), any(), eq(0L));
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 9c421ba..7ae5a11 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -1062,6 +1062,8 @@
         mWm.mAnimator.ready();
         if (!mWm.mWindowPlacerLocked.isTraversalScheduled()) {
             mRootWindowContainer.performSurfacePlacement();
+        } else {
+            waitHandlerIdle(mWm.mAnimationHandler);
         }
         waitUntilWindowAnimatorIdle();
     }
diff --git a/test-mock/src/android/test/mock/MockContext.java b/test-mock/src/android/test/mock/MockContext.java
index f5f9d97..cf38bea 100644
--- a/test-mock/src/android/test/mock/MockContext.java
+++ b/test-mock/src/android/test/mock/MockContext.java
@@ -822,12 +822,6 @@
         throw new UnsupportedOperationException();
     }
 
-    /** {@hide} */
-    @Override
-    public int getUserId() {
-        throw new UnsupportedOperationException();
-    }
-
     @Override
     public Context createConfigurationContext(Configuration overrideConfiguration) {
         throw new UnsupportedOperationException();
diff --git a/tests/Camera2Tests/CameraToo/tests/Android.bp b/tests/Camera2Tests/CameraToo/tests/Android.bp
new file mode 100644
index 0000000..8339a2c
--- /dev/null
+++ b/tests/Camera2Tests/CameraToo/tests/Android.bp
@@ -0,0 +1,34 @@
+// Copyright (C) 2014 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: [
+        "frameworks_base_license",
+    ],
+}
+
+android_test {
+    name: "CameraTooTests",
+    instrumentation_for: "CameraToo",
+    srcs: ["src/**/*.java"],
+    sdk_version: "current",
+    static_libs: [
+        "androidx.test.rules",
+        "mockito-target-minus-junit4",
+    ],
+    data: [
+        ":CameraToo",
+    ],
+}
diff --git a/tests/Camera2Tests/CameraToo/tests/Android.mk b/tests/Camera2Tests/CameraToo/tests/Android.mk
deleted file mode 100644
index dfa64f1..0000000
--- a/tests/Camera2Tests/CameraToo/tests/Android.mk
+++ /dev/null
@@ -1,28 +0,0 @@
-# Copyright (C) 2014 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)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := tests
-LOCAL_PACKAGE_NAME := CameraTooTests
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE  := $(LOCAL_PATH)/../../../../NOTICE
-LOCAL_INSTRUMENTATION_FOR := CameraToo
-LOCAL_SDK_VERSION := current
-LOCAL_SRC_FILES := $(call all-java-files-under,src)
-LOCAL_STATIC_JAVA_LIBRARIES := androidx.test.rules mockito-target-minus-junit4
-
-include $(BUILD_PACKAGE)
diff --git a/tests/Camera2Tests/CameraToo/tests/AndroidTest.xml b/tests/Camera2Tests/CameraToo/tests/AndroidTest.xml
new file mode 100644
index 0000000..884c095
--- /dev/null
+++ b/tests/Camera2Tests/CameraToo/tests/AndroidTest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs CameraToo tests.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CameraTooTests.apk" />
+        <option name="test-file-name" value="CameraToo.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.example.android.camera2.cameratoo.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/tools/aapt2/integration-tests/NamespaceTest/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/Android.mk
deleted file mode 100644
index 6361f9b..0000000
--- a/tools/aapt2/integration-tests/NamespaceTest/Android.mk
+++ /dev/null
@@ -1,2 +0,0 @@
-LOCAL_PATH := $(call my-dir)
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tools/aapt2/integration-tests/NamespaceTest/App/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/App/Android.mk
deleted file mode 100644
index 98b7440..0000000
--- a/tools/aapt2/integration-tests/NamespaceTest/App/Android.mk
+++ /dev/null
@@ -1,33 +0,0 @@
-#
-# Copyright (C) 2017 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)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_PACKAGE_NAME := AaptTestNamespace_App
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE  := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_EXPORT_PACKAGE_RESOURCES := true
-LOCAL_MODULE_TAGS := tests
-LOCAL_SRC_FILES := $(call all-java-files-under,src)
-LOCAL_STATIC_ANDROID_LIBRARIES := \
-    AaptTestNamespace_LibOne \
-    AaptTestNamespace_LibTwo
-include $(BUILD_PACKAGE)
diff --git a/tools/aapt2/integration-tests/NamespaceTest/LibOne/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/LibOne/Android.mk
deleted file mode 100644
index dd41702..0000000
--- a/tools/aapt2/integration-tests/NamespaceTest/LibOne/Android.mk
+++ /dev/null
@@ -1,33 +0,0 @@
-#
-# Copyright (C) 2017 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)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_MODULE := AaptTestNamespace_LibOne
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_MODULE_TAGS := tests
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-LOCAL_MIN_SDK_VERSION := 21
-
-# We need this to retain the R.java generated for this library.
-LOCAL_JAR_EXCLUDE_FILES := none
-include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tools/aapt2/integration-tests/NamespaceTest/LibTwo/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/LibTwo/Android.mk
deleted file mode 100644
index 0d11bcb..0000000
--- a/tools/aapt2/integration-tests/NamespaceTest/LibTwo/Android.mk
+++ /dev/null
@@ -1,34 +0,0 @@
-#
-# Copyright (C) 2017 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)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_MODULE := AaptTestNamespace_LibTwo
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_MODULE_TAGS := tests
-LOCAL_SRC_FILES := $(call all-java-files-under,src)
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-LOCAL_MIN_SDK_VERSION := 21
-
-# We need this to retain the R.java generated for this library.
-LOCAL_JAR_EXCLUDE_FILES := none
-include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tools/aapt2/integration-tests/NamespaceTest/Split/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/Split/Android.mk
deleted file mode 100644
index 30375728..0000000
--- a/tools/aapt2/integration-tests/NamespaceTest/Split/Android.mk
+++ /dev/null
@@ -1,32 +0,0 @@
-#
-# Copyright (C) 2017 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)
-
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_NAMESPACES := true
-LOCAL_PACKAGE_NAME := AaptTestNamespace_Split
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE  := $(LOCAL_PATH)/../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_MODULE_TAGS := tests
-LOCAL_SRC_FILES := $(call all-java-files-under,src)
-LOCAL_APK_LIBRARIES := AaptTestNamespace_App
-LOCAL_RES_LIBRARIES := AaptTestNamespace_App
-LOCAL_AAPT_FLAGS := --package-id 0x80 --rename-manifest-package com.android.aapt.namespace.app
-include $(BUILD_PACKAGE)