Merge "Remove the OverlayInfos without impacts target set"
diff --git a/apct-tests/perftests/core/src/android/libcore/XmlSerializerPerfTest.java b/apct-tests/perftests/core/src/android/libcore/XmlSerializerPerfTest.java
new file mode 100644
index 0000000..412cb5a
--- /dev/null
+++ b/apct-tests/perftests/core/src/android/libcore/XmlSerializerPerfTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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 android.libcore;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Xml;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import libcore.util.XmlObjectFactory;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Compares various kinds of method invocation.
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class XmlSerializerPerfTest {
+
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    @Test
+    public void timeFastSerializer_nonIndent_depth100() throws IOException {
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            XmlSerializer serializer = Xml.newFastSerializer();
+            runTest(serializer, 100);
+        }
+    }
+
+    @Test
+    public void timeFastSerializer_indent_depth100() throws IOException {
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            XmlSerializer serializer = Xml.newFastSerializer();
+            serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+            runTest(serializer, 100);
+        }
+    }
+
+    @Test
+    public void timeKXmlSerializer_nonIndent_depth100() throws IOException {
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            XmlSerializer serializer = XmlObjectFactory.newXmlSerializer();
+            runTest(serializer, 100);
+        }
+    }
+
+    @Test
+    public void timeKXmlSerializer_indent_depth100() throws IOException {
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            XmlSerializer serializer = XmlObjectFactory.newXmlSerializer();
+            serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+            runTest(serializer, 100);
+        }
+    }
+
+    private void runTest(XmlSerializer serializer, int depth) throws IOException {
+        File file = File.createTempFile(XmlSerializerPerfTest.class.getSimpleName(), "tmp");
+        try (OutputStream out = new FileOutputStream(file)) {
+            serializer.setOutput(out, StandardCharsets.UTF_8.name());
+            serializer.startDocument(null, true);
+            writeContent(serializer, depth);
+            serializer.endDocument();
+        }
+    }
+
+    private void writeContent(XmlSerializer serializer, int depth) throws IOException {
+        serializer.startTag(null, "tag");
+        serializer.attribute(null, "attribute", "value1");
+        if (depth > 0) {
+            writeContent(serializer, depth - 1);
+        }
+        serializer.endTag(null, "tag");
+    }
+
+}
diff --git a/core/api/current.txt b/core/api/current.txt
index c70fa37..cc14512 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -23192,6 +23192,7 @@
     method public int describeContents();
     method @Nullable public String getClientPackageName();
     method public int getConnectionState();
+    method @NonNull public java.util.Set<java.lang.String> getDeduplicationIds();
     method @Nullable public CharSequence getDescription();
     method @Nullable public android.os.Bundle getExtras();
     method @NonNull public java.util.List<java.lang.String> getFeatures();
@@ -23225,6 +23226,7 @@
     method @NonNull public android.media.MediaRoute2Info.Builder clearFeatures();
     method @NonNull public android.media.MediaRoute2Info.Builder setClientPackageName(@Nullable String);
     method @NonNull public android.media.MediaRoute2Info.Builder setConnectionState(int);
+    method @NonNull public android.media.MediaRoute2Info.Builder setDeduplicationIds(@NonNull java.util.Set<java.lang.String>);
     method @NonNull public android.media.MediaRoute2Info.Builder setDescription(@Nullable CharSequence);
     method @NonNull public android.media.MediaRoute2Info.Builder setExtras(@Nullable android.os.Bundle);
     method @NonNull public android.media.MediaRoute2Info.Builder setIconUri(@Nullable android.net.Uri);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 1e4023e..186e5be 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -134,7 +134,7 @@
     method public static void resumeAppSwitches() throws android.os.RemoteException;
     method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public void scheduleApplicationInfoChanged(java.util.List<java.lang.String>, int);
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public void setStopUserOnSwitch(int);
-    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean startUserInBackgroundOnSecondaryDisplay(int, int);
+    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public boolean startUserInBackgroundOnSecondaryDisplay(int, int);
     method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public boolean stopUser(int, boolean);
     method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public boolean updateMccMncConfiguration(@NonNull String, @NonNull String);
     method @RequiresPermission(android.Manifest.permission.DUMP) public void waitForBroadcastIdle();
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 576b572..cb7b478 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -4401,7 +4401,7 @@
      */
     @TestApi
     @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
-            android.Manifest.permission.CREATE_USERS})
+            android.Manifest.permission.INTERACT_ACROSS_USERS})
     public boolean startUserInBackgroundOnSecondaryDisplay(@UserIdInt int userId,
             int displayId) {
         if (!UserManager.isUsersOnSecondaryDisplaysEnabled()) {
diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java
index c2df802..13da190 100644
--- a/core/java/android/app/BroadcastOptions.java
+++ b/core/java/android/app/BroadcastOptions.java
@@ -16,6 +16,7 @@
 
 package android.app;
 
+import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -34,6 +35,10 @@
 import android.os.PowerExemptionManager.ReasonCode;
 import android.os.PowerExemptionManager.TempAllowListType;
 
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
 
 /**
@@ -59,6 +64,8 @@
     private boolean mIsAlarmBroadcast = false;
     private long mIdForResponseEvent;
     private @Nullable IntentFilter mRemoveMatchingFilter;
+    private @DeliveryGroupPolicy int mDeliveryGroupPolicy;
+    private @Nullable String mDeliveryGroupKey;
 
     /**
      * Change ID which is invalid.
@@ -190,6 +197,46 @@
     private static final String KEY_REMOVE_MATCHING_FILTER =
             "android:broadcast.removeMatchingFilter";
 
+    /**
+     * Corresponds to {@link #setDeliveryGroupPolicy(int)}.
+     */
+    private static final String KEY_DELIVERY_GROUP_POLICY =
+            "android:broadcast.deliveryGroupPolicy";
+
+    /**
+     * Corresponds to {@link #setDeliveryGroupKey(String, String)}.
+     */
+    private static final String KEY_DELIVERY_GROUP_KEY =
+            "android:broadcast.deliveryGroupKey";
+
+    /**
+     * The list of delivery group policies which specify how multiple broadcasts belonging to
+     * the same delivery group has to be handled.
+     * @hide
+     */
+    @IntDef(flag = true, prefix = { "DELIVERY_GROUP_POLICY_" }, value = {
+            DELIVERY_GROUP_POLICY_ALL,
+            DELIVERY_GROUP_POLICY_MOST_RECENT,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DeliveryGroupPolicy {}
+
+    /**
+     * Delivery group policy that indicates that all the broadcasts in the delivery group
+     * need to be delivered as is.
+     *
+     * @hide
+     */
+    public static final int DELIVERY_GROUP_POLICY_ALL = 0;
+
+    /**
+     * Delivery group policy that indicates that only the most recent broadcast in the delivery
+     * group need to be delivered and the rest can be dropped.
+     *
+     * @hide
+     */
+    public static final int DELIVERY_GROUP_POLICY_MOST_RECENT = 1;
+
     public static BroadcastOptions makeBasic() {
         BroadcastOptions opts = new BroadcastOptions();
         return opts;
@@ -236,6 +283,9 @@
         mIsAlarmBroadcast = opts.getBoolean(KEY_ALARM_BROADCAST, false);
         mRemoveMatchingFilter = opts.getParcelable(KEY_REMOVE_MATCHING_FILTER,
                 IntentFilter.class);
+        mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY,
+                DELIVERY_GROUP_POLICY_ALL);
+        mDeliveryGroupKey = opts.getString(KEY_DELIVERY_GROUP_KEY);
     }
 
     /**
@@ -639,6 +689,41 @@
     }
 
     /**
+     * Set delivery group policy for this broadcast to specify how multiple broadcasts belonging to
+     * the same delivery group has to be handled.
+     *
+     * @hide
+     */
+    public void setDeliveryGroupPolicy(@DeliveryGroupPolicy int policy) {
+        mDeliveryGroupPolicy = policy;
+    }
+
+    /** @hide */
+    public @DeliveryGroupPolicy int getDeliveryGroupPolicy() {
+        return mDeliveryGroupPolicy;
+    }
+
+    /**
+     * Set namespace and key to identify the delivery group that this broadcast belongs to.
+     * If no namespace and key is set, then by default {@link Intent#filterEquals(Intent)} will be
+     * used to identify the delivery group.
+     *
+     * @hide
+     */
+    public void setDeliveryGroupKey(@NonNull String namespace, @NonNull String key) {
+        Preconditions.checkArgument(!namespace.contains("/"),
+                "namespace should not contain '/'");
+        Preconditions.checkArgument(!key.contains("/"),
+                "key should not contain '/'");
+        mDeliveryGroupKey = namespace + "/" + key;
+    }
+
+    /** @hide */
+    public String getDeliveryGroupKey() {
+        return mDeliveryGroupKey;
+    }
+
+    /**
      * Returns the created options as a Bundle, which can be passed to
      * {@link android.content.Context#sendBroadcast(android.content.Intent)
      * Context.sendBroadcast(Intent)} and related methods.
@@ -686,6 +771,12 @@
         if (mRemoveMatchingFilter != null) {
             b.putParcelable(KEY_REMOVE_MATCHING_FILTER, mRemoveMatchingFilter);
         }
+        if (mDeliveryGroupPolicy != DELIVERY_GROUP_POLICY_ALL) {
+            b.putInt(KEY_DELIVERY_GROUP_POLICY, mDeliveryGroupPolicy);
+        }
+        if (mDeliveryGroupKey != null) {
+            b.putString(KEY_DELIVERY_GROUP_KEY, mDeliveryGroupKey);
+        }
         return b.isEmpty() ? null : b;
     }
 }
diff --git a/core/java/android/app/backup/BackupRestoreEventLogger.java b/core/java/android/app/backup/BackupRestoreEventLogger.java
new file mode 100644
index 0000000..b789b38
--- /dev/null
+++ b/core/java/android/app/backup/BackupRestoreEventLogger.java
@@ -0,0 +1,280 @@
+/*
+ * 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 android.app.backup;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+// TODO(b/244436184): Make this @SystemApi
+/**
+ * Class to log B&R stats for each data type that is backed up and restored by the calling app.
+ *
+ * The logger instance is designed to accept a limited number of unique
+ * {link @BackupRestoreDataType} values, as determined by the underlying implementation. Apps are
+ * expected to have a small pre-defined set of data type values they use. Attempts to log too many
+ * unique values will be rejected.
+ *
+ * @hide
+ */
+public class BackupRestoreEventLogger {
+    /**
+     * Max number of unique data types for which an instance of this logger can store info. Attempts
+     * to use more distinct data type values will be rejected.
+     */
+    public static final int DATA_TYPES_ALLOWED = 15;
+
+    /**
+     * Operation types for which this logger can be used.
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            OperationType.BACKUP,
+            OperationType.RESTORE
+    })
+    @interface OperationType {
+        int BACKUP = 1;
+        int RESTORE = 2;
+    }
+
+    /**
+     * Denotes that the annotated element identifies a data type as required by the logging methods
+     * of {@code BackupRestoreEventLogger}
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BackupRestoreDataType {}
+
+    /**
+     * Denotes that the annotated element identifies an error type as required by the logging
+     * methods of {@code BackupRestoreEventLogger}
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BackupRestoreError {}
+
+    private final int mOperationType;
+
+    /**
+     * @param operationType type of the operation for which logging will be performed. See
+     *                      {@link OperationType}. Attempts to use logging methods that don't match
+     *                      the specified operation type will be rejected (e.g. use backup methods
+     *                      for a restore logger and vice versa).
+     */
+    public BackupRestoreEventLogger(@OperationType int operationType) {
+        mOperationType = operationType;
+    }
+
+    /**
+     * Report progress during a backup operation. Call this method for each distinct data type that
+     * your {@code BackupAgent} implementation handles for any items of that type that have been
+     * successfully backed up. Repeated calls to this method with the same {@code dataType} will
+     * increase the total count of items associated with this data type by {@code count}.
+     *
+     * This method should be called from a {@link BackupAgent} implementation during an ongoing
+     * backup operation.
+     *
+     * @param dataType the type of data being backed.
+     * @param count number of items of the given type that have been successfully backed up.
+     *
+     * @return boolean, indicating whether the log has been accepted.
+     */
+    public boolean logItemsBackedUp(@NonNull @BackupRestoreDataType String dataType, int count) {
+        return true;
+    }
+
+    /**
+     * Report errors during a backup operation. Call this method whenever items of a certain data
+     * type failed to back up. Repeated calls to this method with the same {@code dataType} /
+     * {@code error} will increase the total count of items associated with this data type / error
+     * by {@code count}.
+     *
+     * This method should be called from a {@link BackupAgent} implementation during an ongoing
+     * backup operation.
+     *
+     * @param dataType the type of data being backed.
+     * @param count number of items of the given type that have failed to back up.
+     * @param error optional, the error that has caused the failure.
+     *
+     * @return boolean, indicating whether the log has been accepted.
+     */
+    public boolean logItemsBackupFailed(@NonNull @BackupRestoreDataType String dataType, int count,
+            @Nullable @BackupRestoreError String error) {
+        return true;
+    }
+
+    /**
+     * Report metadata associated with a data type that is currently being backed up, e.g. name of
+     * the selected wallpaper file / package. Repeated calls to this method with the same {@code
+     * dataType} will overwrite the previously supplied {@code metaData} value.
+     *
+     * The logger does not store or transmit the provided metadata value. Instead, it’s replaced
+     * with the SHA-256 hash of the provided string.
+     *
+     * This method should be called from a {@link BackupAgent} implementation during an ongoing
+     * backup operation.
+     *
+     * @param dataType the type of data being backed up.
+     * @param metaData the metadata associated with the data type.
+     *
+     * @return boolean, indicating whether the log has been accepted.
+     */
+    public boolean logBackupMetaData(@NonNull @BackupRestoreDataType String dataType,
+            @NonNull String metaData) {
+        return true;
+    }
+
+    /**
+     * Report progress during a restore operation. Call this method for each distinct data type that
+     * your {@code BackupAgent} implementation handles if any items of that type have been
+     * successfully restored. Repeated calls to this method with the same {@code dataType} will
+     * increase the total count of items associated with this data type by {@code count}.
+     *
+     * This method should either be called from a {@link BackupAgent} implementation during an
+     * ongoing restore operation or during any delayed restore actions the package had scheduled
+     * earlier (e.g. complete the restore once a certain dependency becomes available on the
+     * device).
+     *
+     * @param dataType the type of data being restored.
+     * @param count number of items of the given type that have been successfully restored.
+     *
+     * @return boolean, indicating whether the log has been accepted.
+     */
+    public boolean logItemsRestored(@NonNull @BackupRestoreDataType String dataType, int count) {
+        return true;
+    }
+
+    /**
+     * Report errors during a restore operation. Call this method whenever items of a certain data
+     * type failed to restore. Repeated calls to this method with the same {@code dataType} /
+     * {@code error} will increase the total count of items associated with this data type / error
+     * by {@code count}.
+     *
+     * This method should either be called from a {@link BackupAgent} implementation during an
+     * ongoing restore operation or during any delayed restore actions the package had scheduled
+     * earlier (e.g. complete the restore once a certain dependency becomes available on the
+     * device).
+     *
+     * @param dataType the type of data being restored.
+     * @param count number of items of the given type that have failed to restore.
+     * @param error optional, the error that has caused the failure.
+     *
+     * @return boolean, indicating whether the log has been accepted.
+     */
+    public boolean logItemsRestoreFailed(@NonNull @BackupRestoreDataType String dataType, int count,
+            @Nullable @BackupRestoreError String error) {
+        return true;
+    }
+
+    /**
+     * Report metadata associated with a data type that is currently being restored, e.g. name of
+     * the selected wallpaper file / package. Repeated calls to this method with the same
+     * {@code dataType} will overwrite the previously supplied {@code metaData} value.
+     *
+     * The logger does not store or transmit the provided metadata value. Instead, it’s replaced
+     * with the SHA-256 hash of the provided string.
+     *
+     * This method should either be called from a {@link BackupAgent} implementation during an
+     * ongoing restore operation or during any delayed restore actions the package had scheduled
+     * earlier (e.g. complete the restore once a certain dependency becomes available on the
+     * device).
+     *
+     * @param dataType the type of data being restored.
+     * @param metadata the metadata associated with the data type.
+     *
+     * @return boolean, indicating whether the log has been accepted.
+     */
+    public boolean logRestoreMetadata(@NonNull @BackupRestoreDataType String dataType,
+            @NonNull  String metadata) {
+        return true;
+    }
+
+    /**
+     * Get the contents of this logger. This method should only be used by B&R code in Android
+     * Framework.
+     *
+     * @hide
+     */
+    public List<DataTypeResult> getLoggingResults() {
+        return Collections.emptyList();
+    }
+
+    /**
+     * Get the operation type for which this logger was created. This method should only be used
+     * by B&R code in Android Framework.
+     *
+     * @hide
+     */
+    public @OperationType int getOperationType() {
+        return mOperationType;
+    }
+
+    /**
+     * Encapsulate logging results for a single data type.
+     */
+    public static class DataTypeResult {
+        @BackupRestoreDataType
+        private final String mDataType;
+        private final int mSuccessCount;
+        private final Map<String, Integer> mErrors;
+        private final byte[] mMetadataHash;
+
+        public DataTypeResult(String dataType, int successCount,
+                Map<String, Integer> errors, byte[] metadataHash) {
+            mDataType = dataType;
+            mSuccessCount = successCount;
+            mErrors = errors;
+            mMetadataHash = metadataHash;
+        }
+
+        @NonNull
+        @BackupRestoreDataType
+        public String getDataType() {
+            return mDataType;
+        }
+
+        /**
+         * @return number of items of the given data type that have been successfully backed up or
+         *         restored.
+         */
+        public int getSuccessCount() {
+            return mSuccessCount;
+        }
+
+        /**
+         * @return mapping of {@link BackupRestoreError} to the count of items that are affected by
+         *         the error.
+         */
+        @NonNull
+        public Map<String, Integer> getErrors() {
+            return mErrors;
+        }
+
+        /**
+         * @return SHA-256 hash of the metadata or {@code null} of no metadata has been logged for
+         *         this data type.
+         */
+        @Nullable
+        public byte[] getMetadataHash() {
+            return mMetadataHash;
+        }
+    }
+}
diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java
index cc303fb..24e47bf 100644
--- a/core/java/android/appwidget/AppWidgetHost.java
+++ b/core/java/android/appwidget/AppWidgetHost.java
@@ -418,14 +418,7 @@
         AppWidgetHostView view = onCreateView(context, appWidgetId, appWidget);
         view.setInteractionHandler(mInteractionHandler);
         view.setAppWidget(appWidgetId, appWidget);
-        addListener(appWidgetId, view);
-        RemoteViews views;
-        try {
-            views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId);
-        } catch (RemoteException e) {
-            throw new RuntimeException("system server dead?", e);
-        }
-        view.updateAppWidget(views);
+        setListener(appWidgetId, view);
 
         return view;
     }
@@ -513,13 +506,19 @@
      * The AppWidgetHost retains a pointer to the newly-created listener.
      * @param appWidgetId The ID of the app widget for which to add the listener
      * @param listener The listener interface that deals with actions towards the widget view
-     *
      * @hide
      */
-    public void addListener(int appWidgetId, @NonNull AppWidgetHostListener listener) {
+    public void setListener(int appWidgetId, @NonNull AppWidgetHostListener listener) {
         synchronized (mListeners) {
             mListeners.put(appWidgetId, listener);
         }
+        RemoteViews views = null;
+        try {
+            views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId);
+        } catch (RemoteException e) {
+            throw new RuntimeException("system server dead?", e);
+        }
+        listener.updateAppWidget(views);
     }
 
     /**
diff --git a/core/java/android/credentials/ui/Constants.java b/core/java/android/credentials/ui/Constants.java
new file mode 100644
index 0000000..aeeede7
--- /dev/null
+++ b/core/java/android/credentials/ui/Constants.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 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 android.credentials.ui;
+
+/**
+ * Constants for the ui protocol that doesn't fit into other individual data structures.
+ *
+ * @hide
+ */
+public class Constants {
+
+    /**
+    * The intent extra key for the {@code ResultReceiver} object when launching the UX
+    * activities.
+    */
+    public static final String EXTRA_RESULT_RECEIVER =
+            "android.credentials.ui.extra.RESULT_RECEIVER";
+
+}
diff --git a/core/java/android/credentials/ui/IntentFactory.java b/core/java/android/credentials/ui/IntentFactory.java
new file mode 100644
index 0000000..9a038d1
--- /dev/null
+++ b/core/java/android/credentials/ui/IntentFactory.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 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 android.credentials.ui;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.ResultReceiver;
+
+import java.util.ArrayList;
+
+/**
+ * Helpers for generating the intents and related extras parameters to launch the UI activities.
+ *
+ * @hide
+ */
+public class IntentFactory {
+    /** Generate a new launch intent to the . */
+    public static Intent newIntent(RequestInfo requestInfo,
+            ArrayList<ProviderData> providerDataList, ResultReceiver resultReceiver) {
+        Intent intent = new Intent();
+        // TODO: define these as proper config strings.
+        String activityName = "com.androidauth.tatiaccountselector/.CredentialSelectorActivity";
+        // String activityName = "com.android.credentialmanager/.CredentialSelectorActivity";
+        intent.setComponent(ComponentName.unflattenFromString(activityName));
+
+        intent.putParcelableArrayListExtra(
+                ProviderData.EXTRA_PROVIDER_DATA_LIST, providerDataList);
+        intent.putExtra(RequestInfo.EXTRA_REQUEST_INFO, requestInfo);
+        intent.putExtra(Constants.EXTRA_RESULT_RECEIVER,
+                toIpcFriendlyResultReceiver(resultReceiver));
+
+        return intent;
+    }
+
+    /**
+    * Convert an instance of a "locally-defined" ResultReceiver to an instance of
+    * {@link android.os.ResultReceiver} itself, which the receiving process will be able to
+    * unmarshall.
+    */
+    private static <T extends ResultReceiver> ResultReceiver toIpcFriendlyResultReceiver(
+            T resultReceiver) {
+        final Parcel parcel = Parcel.obtain();
+        resultReceiver.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+
+        return ipcFriendly;
+    }
+
+    private IntentFactory() {}
+}
diff --git a/core/java/android/credentials/ui/ProviderData.java b/core/java/android/credentials/ui/ProviderData.java
index 38bd4e5..35e12fa 100644
--- a/core/java/android/credentials/ui/ProviderData.java
+++ b/core/java/android/credentials/ui/ProviderData.java
@@ -16,8 +16,10 @@
 
 package android.credentials.ui;
 
+import android.annotation.CurrentTimeMillisLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.drawable.Icon;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -43,30 +45,49 @@
     @NonNull
     private final String mProviderId;
     @NonNull
+    private final String mProviderDisplayName;
+    @NonNull
+    private final Icon mIcon;
+    @NonNull
     private final List<Entry> mCredentialEntries;
     @NonNull
     private final List<Entry> mActionChips;
     @Nullable
     private final Entry mAuthenticationEntry;
 
+    private final @CurrentTimeMillisLong long mLastUsedTimeMillis;
+
     public ProviderData(
-            @NonNull String providerId,
-            @NonNull List<Entry> credentialEntries,
-            @NonNull List<Entry> actionChips,
-            @Nullable Entry authenticationEntry) {
+            @NonNull String providerId, @NonNull String providerDisplayName,
+            @NonNull Icon icon, @NonNull List<Entry> credentialEntries,
+            @NonNull List<Entry> actionChips, @Nullable Entry authenticationEntry,
+            @CurrentTimeMillisLong long lastUsedTimeMillis) {
         mProviderId = providerId;
+        mProviderDisplayName = providerDisplayName;
+        mIcon = icon;
         mCredentialEntries = credentialEntries;
         mActionChips = actionChips;
         mAuthenticationEntry = authenticationEntry;
+        mLastUsedTimeMillis = lastUsedTimeMillis;
     }
 
-    /** Returns the provider package name. */
+    /** Returns the unique provider id. */
     @NonNull
     public String getProviderId() {
         return mProviderId;
     }
 
     @NonNull
+    public String getProviderDisplayName() {
+        return mProviderDisplayName;
+    }
+
+    @NonNull
+    public Icon getIcon() {
+        return mIcon;
+    }
+
+    @NonNull
     public List<Entry> getCredentialEntries() {
         return mCredentialEntries;
     }
@@ -81,11 +102,24 @@
         return mAuthenticationEntry;
     }
 
+    /** Returns the time when the provider was last used. */
+    public @CurrentTimeMillisLong long getLastUsedTimeMillis() {
+        return mLastUsedTimeMillis;
+    }
+
     protected ProviderData(@NonNull Parcel in) {
         String providerId = in.readString8();
         mProviderId = providerId;
         AnnotationValidations.validate(NonNull.class, null, mProviderId);
 
+        String providerDisplayName = in.readString8();
+        mProviderDisplayName = providerDisplayName;
+        AnnotationValidations.validate(NonNull.class, null, mProviderDisplayName);
+
+        Icon icon = in.readTypedObject(Icon.CREATOR);
+        mIcon = icon;
+        AnnotationValidations.validate(NonNull.class, null, mIcon);
+
         List<Entry> credentialEntries = new ArrayList<>();
         in.readTypedList(credentialEntries, Entry.CREATOR);
         mCredentialEntries = credentialEntries;
@@ -98,14 +132,20 @@
 
         Entry authenticationEntry = in.readTypedObject(Entry.CREATOR);
         mAuthenticationEntry = authenticationEntry;
+
+        long lastUsedTimeMillis = in.readLong();
+        mLastUsedTimeMillis = lastUsedTimeMillis;
     }
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeString8(mProviderId);
+        dest.writeString8(mProviderDisplayName);
+        dest.writeTypedObject(mIcon, flags);
         dest.writeTypedList(mCredentialEntries);
         dest.writeTypedList(mActionChips);
         dest.writeTypedObject(mAuthenticationEntry, flags);
+        dest.writeLong(mLastUsedTimeMillis);
     }
 
     @Override
@@ -124,4 +164,83 @@
             return new ProviderData[size];
         }
     };
+
+    /**
+     * Builder for {@link ProviderData}.
+     *
+     * @hide
+     */
+    public static class Builder {
+        private @NonNull String mProviderId;
+        private @NonNull String mProviderDisplayName;
+        private @NonNull Icon mIcon;
+        private @NonNull List<Entry> mCredentialEntries = new ArrayList<>();
+        private @NonNull List<Entry> mActionChips = new ArrayList<>();
+        private @Nullable Entry mAuthenticationEntry = null;
+        private @CurrentTimeMillisLong long mLastUsedTimeMillis = 0L;
+
+        /** Constructor with required properties. */
+        public Builder(@NonNull String providerId, @NonNull String providerDisplayName,
+                @NonNull Icon icon) {
+            mProviderId = providerId;
+            mProviderDisplayName = providerDisplayName;
+            mIcon = icon;
+        }
+
+        /** Sets the unique provider id. */
+        @NonNull
+        public Builder setProviderId(@NonNull String providerId) {
+            mProviderId = providerId;
+            return this;
+        }
+
+        /** Sets the provider display name to be displayed to the user. */
+        @NonNull
+        public Builder setProviderDisplayName(@NonNull String providerDisplayName) {
+            mProviderDisplayName = providerDisplayName;
+            return this;
+        }
+
+        /** Sets the provider icon to be displayed to the user. */
+        @NonNull
+        public Builder setIcon(@NonNull Icon icon) {
+            mIcon = icon;
+            return this;
+        }
+
+        /** Sets the list of save / get credential entries to be displayed to the user. */
+        @NonNull
+        public Builder setCredentialEntries(@NonNull List<Entry> credentialEntries) {
+            mCredentialEntries = credentialEntries;
+            return this;
+        }
+
+        /** Sets the list of action chips to be displayed to the user. */
+        @NonNull
+        public Builder setActionChips(@NonNull List<Entry> actionChips) {
+            mActionChips = actionChips;
+            return this;
+        }
+
+        /** Sets the authentication entry to be displayed to the user. */
+        @NonNull
+        public Builder setAuthenticationEntry(@Nullable Entry authenticationEntry) {
+            mAuthenticationEntry = authenticationEntry;
+            return this;
+        }
+
+        /** Sets the time when the provider was last used. */
+        @NonNull
+        public Builder setLastUsedTimeMillis(@CurrentTimeMillisLong long lastUsedTimeMillis) {
+            mLastUsedTimeMillis = lastUsedTimeMillis;
+            return this;
+        }
+
+        /** Builds a {@link ProviderData}. */
+        @NonNull
+        public ProviderData build() {
+            return new ProviderData(mProviderId, mProviderDisplayName, mIcon, mCredentialEntries,
+                mActionChips, mAuthenticationEntry, mLastUsedTimeMillis);
+        }
+    }
 }
diff --git a/core/java/android/credentials/ui/RequestInfo.java b/core/java/android/credentials/ui/RequestInfo.java
index 5de6d73..eddb519 100644
--- a/core/java/android/credentials/ui/RequestInfo.java
+++ b/core/java/android/credentials/ui/RequestInfo.java
@@ -36,12 +36,6 @@
      */
     public static final @NonNull String EXTRA_REQUEST_INFO =
             "android.credentials.ui.extra.REQUEST_INFO";
-    /**
-     * The intent extra key for the {@code ResultReceiver} object when launching the UX
-     * activities.
-     */
-    public static final @NonNull String EXTRA_RESULT_RECEIVER =
-            "android.credentials.ui.extra.RESULT_RECEIVER";
 
     /** Type value for an executeGetCredential request. */
     public static final @NonNull String TYPE_GET = "android.credentials.ui.TYPE_GET";
diff --git a/core/java/android/hardware/input/VirtualDpad.java b/core/java/android/hardware/input/VirtualDpad.java
index d7cda9e..4d61553 100644
--- a/core/java/android/hardware/input/VirtualDpad.java
+++ b/core/java/android/hardware/input/VirtualDpad.java
@@ -24,7 +24,6 @@
 import android.os.RemoteException;
 import android.view.KeyEvent;
 
-import java.io.Closeable;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
@@ -39,7 +38,7 @@
  * @hide
  */
 @SystemApi
-public class VirtualDpad implements Closeable {
+public class VirtualDpad extends VirtualInputDevice {
 
     private final Set<Integer> mSupportedKeyCodes =
             Collections.unmodifiableSet(
@@ -50,23 +49,10 @@
                                     KeyEvent.KEYCODE_DPAD_LEFT,
                                     KeyEvent.KEYCODE_DPAD_RIGHT,
                                     KeyEvent.KEYCODE_DPAD_CENTER)));
-    private final IVirtualDevice mVirtualDevice;
-    private final IBinder mToken;
 
     /** @hide */
     public VirtualDpad(IVirtualDevice virtualDevice, IBinder token) {
-        mVirtualDevice = virtualDevice;
-        mToken = token;
-    }
-
-    @Override
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void close() {
-        try {
-            mVirtualDevice.unregisterInputDevice(mToken);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        super(virtualDevice, token);
     }
 
     /**
diff --git a/core/java/android/hardware/input/VirtualInputDevice.java b/core/java/android/hardware/input/VirtualInputDevice.java
new file mode 100644
index 0000000..2a79ef0
--- /dev/null
+++ b/core/java/android/hardware/input/VirtualInputDevice.java
@@ -0,0 +1,62 @@
+/*
+ * 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 android.hardware.input;
+
+import android.annotation.RequiresPermission;
+import android.companion.virtual.IVirtualDevice;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.io.Closeable;
+
+/**
+ * The base class for all virtual input devices such as VirtualKeyboard, VirtualMouse.
+ * This implements the shared functionality such as closing the device and keeping track of
+ * identifiers.
+ *
+ * @hide
+ */
+abstract class VirtualInputDevice implements Closeable {
+
+    /**
+     * The virtual device to which this VirtualInputDevice belongs to.
+     */
+    protected final IVirtualDevice mVirtualDevice;
+
+    /**
+     * The token used to uniquely identify the virtual input device.
+     */
+    protected final IBinder mToken;
+
+    /** @hide */
+    VirtualInputDevice(
+            IVirtualDevice virtualDevice, IBinder token) {
+        mVirtualDevice = virtualDevice;
+        mToken = token;
+    }
+
+
+    @Override
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void close() {
+        try {
+            mVirtualDevice.unregisterInputDevice(mToken);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/core/java/android/hardware/input/VirtualKeyboard.java b/core/java/android/hardware/input/VirtualKeyboard.java
index 901401fe..e569dbf 100644
--- a/core/java/android/hardware/input/VirtualKeyboard.java
+++ b/core/java/android/hardware/input/VirtualKeyboard.java
@@ -24,8 +24,6 @@
 import android.os.RemoteException;
 import android.view.KeyEvent;
 
-import java.io.Closeable;
-
 /**
  * A virtual keyboard representing a key input mechanism on a remote device, such as a built-in
  * keyboard on a laptop, a software keyboard on a tablet, or a keypad on a TV remote control.
@@ -36,26 +34,13 @@
  * @hide
  */
 @SystemApi
-public class VirtualKeyboard implements Closeable {
+public class VirtualKeyboard extends VirtualInputDevice {
 
     private final int mUnsupportedKeyCode = KeyEvent.KEYCODE_DPAD_CENTER;
-    private final IVirtualDevice mVirtualDevice;
-    private final IBinder mToken;
 
     /** @hide */
     public VirtualKeyboard(IVirtualDevice virtualDevice, IBinder token) {
-        mVirtualDevice = virtualDevice;
-        mToken = token;
-    }
-
-    @Override
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void close() {
-        try {
-            mVirtualDevice.unregisterInputDevice(mToken);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        super(virtualDevice, token);
     }
 
     /**
diff --git a/core/java/android/hardware/input/VirtualMouse.java b/core/java/android/hardware/input/VirtualMouse.java
index 6e2b56a..7eba2b8 100644
--- a/core/java/android/hardware/input/VirtualMouse.java
+++ b/core/java/android/hardware/input/VirtualMouse.java
@@ -25,8 +25,6 @@
 import android.os.RemoteException;
 import android.view.MotionEvent;
 
-import java.io.Closeable;
-
 /**
  * A virtual mouse representing a relative input mechanism on a remote device, such as a mouse or
  * trackpad.
@@ -37,25 +35,11 @@
  * @hide
  */
 @SystemApi
-public class VirtualMouse implements Closeable {
-
-    private final IVirtualDevice mVirtualDevice;
-    private final IBinder mToken;
+public class VirtualMouse extends VirtualInputDevice {
 
     /** @hide */
     public VirtualMouse(IVirtualDevice virtualDevice, IBinder token) {
-        mVirtualDevice = virtualDevice;
-        mToken = token;
-    }
-
-    @Override
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void close() {
-        try {
-            mVirtualDevice.unregisterInputDevice(mToken);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        super(virtualDevice, token);
     }
 
     /**
diff --git a/core/java/android/hardware/input/VirtualTouchscreen.java b/core/java/android/hardware/input/VirtualTouchscreen.java
index c8d602a..0d07753 100644
--- a/core/java/android/hardware/input/VirtualTouchscreen.java
+++ b/core/java/android/hardware/input/VirtualTouchscreen.java
@@ -23,8 +23,6 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 
-import java.io.Closeable;
-
 /**
  * A virtual touchscreen representing a touch-based display input mechanism on a remote device.
  *
@@ -34,25 +32,10 @@
  * @hide
  */
 @SystemApi
-public class VirtualTouchscreen implements Closeable {
-
-    private final IVirtualDevice mVirtualDevice;
-    private final IBinder mToken;
-
+public class VirtualTouchscreen extends VirtualInputDevice {
     /** @hide */
     public VirtualTouchscreen(IVirtualDevice virtualDevice, IBinder token) {
-        mVirtualDevice = virtualDevice;
-        mToken = token;
-    }
-
-    @Override
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void close() {
-        try {
-            mVirtualDevice.unregisterInputDevice(mToken);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        super(virtualDevice, token);
     }
 
     /**
diff --git a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
index 891da24..4f09bee 100644
--- a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
+++ b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
@@ -777,7 +777,7 @@
     }
 
     /**
-     * Invokes {@link IRemoteInputConnection#replaceText(InputConnectionCommandHeader, int, int,
+     * Invokes {@code IRemoteInputConnection#replaceText(InputConnectionCommandHeader, int, int,
      * CharSequence, TextAttribute)}.
      *
      * @param start the character index where the replacement should start.
@@ -788,6 +788,8 @@
      *     that this means you can't position the cursor within the text.
      * @param text the text to replace. This may include styles.
      * @param textAttribute The extra information about the text. This value may be null.
+     * @return {@code true} if the invocation is completed without {@link RemoteException}, {@code
+     *     false} otherwise.
      */
     @AnyThread
     public boolean replaceText(
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 4df0139..d3a6323 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -1246,8 +1246,21 @@
         // If the call was {@link IBinder#FLAG_ONEWAY} then these exceptions
         // disappear into the ether.
         final boolean tagEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_AIDL);
+        final boolean hasFullyQualifiedName = getMaxTransactionId() > 0;
         final String transactionTraceName;
-        if (tagEnabled) {
+
+        if (tagEnabled && hasFullyQualifiedName) {
+            // If tracing enabled and we have a fully qualified name, fetch the name
+            transactionTraceName = getTransactionTraceName(code);
+        } else if (tagEnabled && isStackTrackingEnabled()) {
+            // If tracing is enabled and we *don't* have a fully qualified name, fetch the
+            // 'best effort' name only for stack tracking. This works around noticeable perf impact
+            // on low latency binder calls (<100us). The tracing call itself is between (1-10us) and
+            // the perf impact can be quite noticeable while benchmarking such binder calls.
+            // The primary culprits are ContentProviders and Cursors which convenienty don't
+            // autogenerate their AIDL and hence will not have a fully qualified name.
+            //
+            // TODO(b/253426478): Relax this constraint after a more robust fix
             transactionTraceName = getTransactionTraceName(code);
         } else {
             transactionTraceName = null;
diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java
index f62cc87..8afd6de 100644
--- a/core/java/android/os/PowerManagerInternal.java
+++ b/core/java/android/os/PowerManagerInternal.java
@@ -341,4 +341,10 @@
      * device is not awake.
      */
     public abstract void nap(long eventTime, boolean allowWake);
+
+    /**
+     * Returns true if ambient display is suppressed by any app with any token. This method will
+     * return false if ambient display is not available.
+     */
+    public abstract boolean isAmbientDisplaySuppressed();
 }
diff --git a/core/java/android/service/dreams/DreamActivity.java b/core/java/android/service/dreams/DreamActivity.java
index f6a7c8e..a2fa139 100644
--- a/core/java/android/service/dreams/DreamActivity.java
+++ b/core/java/android/service/dreams/DreamActivity.java
@@ -44,6 +44,8 @@
 public class DreamActivity extends Activity {
     static final String EXTRA_CALLBACK = "binder";
     static final String EXTRA_DREAM_TITLE = "title";
+    @Nullable
+    private DreamService.DreamActivityCallbacks mCallback;
 
     public DreamActivity() {}
 
@@ -57,11 +59,19 @@
         }
 
         final Bundle extras = getIntent().getExtras();
-        final DreamService.DreamActivityCallback callback =
-                (DreamService.DreamActivityCallback) extras.getBinder(EXTRA_CALLBACK);
+        mCallback = (DreamService.DreamActivityCallbacks) extras.getBinder(EXTRA_CALLBACK);
 
-        if (callback != null) {
-            callback.onActivityCreated(this);
+        if (mCallback != null) {
+            mCallback.onActivityCreated(this);
         }
     }
+
+    @Override
+    public void onDestroy() {
+        if (mCallback != null) {
+            mCallback.onActivityDestroyed();
+        }
+
+        super.onDestroy();
+    }
 }
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 3c1fef0..cb0dce9 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -1295,7 +1295,7 @@
             Intent i = new Intent(this, DreamActivity.class);
             i.setPackage(getApplicationContext().getPackageName());
             i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            i.putExtra(DreamActivity.EXTRA_CALLBACK, new DreamActivityCallback(mDreamToken));
+            i.putExtra(DreamActivity.EXTRA_CALLBACK, new DreamActivityCallbacks(mDreamToken));
             final ServiceInfo serviceInfo = fetchServiceInfo(this,
                     new ComponentName(this, getClass()));
             i.putExtra(DreamActivity.EXTRA_DREAM_TITLE, fetchDreamLabel(this, serviceInfo));
@@ -1488,10 +1488,10 @@
     }
 
     /** @hide */
-    final class DreamActivityCallback extends Binder {
+    final class DreamActivityCallbacks extends Binder {
         private final IBinder mActivityDreamToken;
 
-        DreamActivityCallback(IBinder token) {
+        DreamActivityCallbacks(IBinder token) {
             mActivityDreamToken = token;
         }
 
@@ -1516,6 +1516,12 @@
             mActivity = activity;
             onWindowCreated(activity.getWindow());
         }
+
+        // If DreamActivity is destroyed, wake up from Dream.
+        void onActivityDestroyed() {
+            mActivity = null;
+            onDestroy();
+        }
     }
 
     /**
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index b559161..a59d429 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -284,7 +284,6 @@
         private Display mDisplay;
         private Context mDisplayContext;
         private int mDisplayState;
-        private @Surface.Rotation int mDisplayInstallOrientation;
         private float mWallpaperDimAmount = 0.05f;
         private float mPreviousWallpaperDimAmount = mWallpaperDimAmount;
         private float mDefaultDimAmount = mWallpaperDimAmount;
@@ -1159,7 +1158,7 @@
                             mSurfaceControl, mInsetsState, mTempControls, mSyncSeqIdBundle);
 
                     final int transformHint = SurfaceControl.rotationToBufferTransform(
-                            (mDisplayInstallOrientation + mDisplay.getRotation()) % 4);
+                            (mDisplay.getInstallOrientation() + mDisplay.getRotation()) % 4);
                     mSurfaceControl.setTransformHint(transformHint);
                     WindowLayout.computeSurfaceSize(mLayout, maxBounds, mWidth, mHeight,
                             mWinFrames.frame, false /* dragResizing */, mSurfaceSize);
@@ -1420,7 +1419,6 @@
             mWallpaperDimAmount = mDefaultDimAmount;
             mPreviousWallpaperDimAmount = mWallpaperDimAmount;
             mDisplayState = mDisplay.getState();
-            mDisplayInstallOrientation = mDisplay.getInstallOrientation();
 
             if (DEBUG) Log.v(TAG, "onCreate(): " + this);
             onCreate(mSurfaceHolder);
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index a1ece92..a0a07b3 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -162,7 +162,13 @@
                         if (candidateView == getConnectedView()) {
                             startHandwriting(candidateView);
                         } else {
-                            candidateView.requestFocus();
+                            if (candidateView.getRevealOnFocusHint()) {
+                                candidateView.setRevealOnFocusHint(false);
+                                candidateView.requestFocus();
+                                candidateView.setRevealOnFocusHint(true);
+                            } else {
+                                candidateView.requestFocus();
+                            }
                         }
                     }
                 }
diff --git a/core/java/android/view/ImeFocusController.java b/core/java/android/view/ImeFocusController.java
index 4de7c4f..43828d5 100644
--- a/core/java/android/view/ImeFocusController.java
+++ b/core/java/android/view/ImeFocusController.java
@@ -108,10 +108,11 @@
     }
 
     /**
-     * @see InputMethodManager#checkFocus()
+     * @see ViewRootImpl#dispatchCheckFocus()
      */
-    public boolean checkFocus(boolean forceNewFocus, boolean startInput) {
-        return getImmDelegate().checkFocus(forceNewFocus, startInput, mViewRootImpl);
+    @UiThread
+    void onScheduledCheckFocus() {
+        getImmDelegate().onScheduledCheckFocus(mViewRootImpl);
     }
 
     @UiThread
@@ -163,7 +164,7 @@
         void onPostWindowGainedFocus(View viewForWindowFocus,
                 @NonNull WindowManager.LayoutParams windowAttribute);
         void onViewFocusChanged(@NonNull View view, boolean hasFocus);
-        boolean checkFocus(boolean forceNewFocus, boolean startInput, ViewRootImpl viewRootImpl);
+        void onScheduledCheckFocus(@NonNull ViewRootImpl viewRootImpl);
         void onViewDetachedFromWindow(View view, ViewRootImpl viewRootImpl);
         void onWindowDismissed(ViewRootImpl viewRootImpl);
     }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 58c8126..bfa1350 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -428,8 +428,6 @@
     final DisplayManager mDisplayManager;
     final String mBasePackageName;
 
-    private @Surface.Rotation int mDisplayInstallOrientation;
-
     final int[] mTmpLocation = new int[2];
 
     final TypedValue mTmpValue = new TypedValue();
@@ -1134,7 +1132,6 @@
             if (mView == null) {
                 mView = view;
 
-                mDisplayInstallOrientation = mDisplay.getInstallOrientation();
                 mViewLayoutDirectionInitial = mView.getRawLayoutDirection();
                 mFallbackEventHandler.setView(view);
                 mWindowAttributes.copyFrom(attrs);
@@ -1905,7 +1902,6 @@
         updateInternalDisplay(displayId, mView.getResources());
         mImeFocusController.onMovedToDisplay();
         mAttachInfo.mDisplayState = mDisplay.getState();
-        mDisplayInstallOrientation = mDisplay.getInstallOrientation();
         // Internal state updated, now notify the view hierarchy.
         mView.dispatchMovedToDisplay(mDisplay, config);
     }
@@ -5718,7 +5714,7 @@
                     enqueueInputEvent(event, null, 0, true);
                 } break;
                 case MSG_CHECK_FOCUS: {
-                    getImeFocusController().checkFocus(false, true);
+                    getImeFocusController().onScheduledCheckFocus();
                 } break;
                 case MSG_CLOSE_SYSTEM_DIALOGS: {
                     if (mView != null) {
@@ -8235,7 +8231,7 @@
         }
 
         final int transformHint = SurfaceControl.rotationToBufferTransform(
-                (mDisplayInstallOrientation + mDisplay.getRotation()) % 4);
+                (mDisplay.getInstallOrientation() + mDisplay.getRotation()) % 4);
 
         WindowLayout.computeSurfaceSize(mWindowAttributes, winConfig.getMaxBounds(), requestedWidth,
                 requestedHeight, mWinFrameInScreen, mPendingDragResizing, mSurfaceSize);
@@ -8260,7 +8256,7 @@
         }
 
         mLastTransformHint = transformHint;
-      
+
         mSurfaceControl.setTransformHint(transformHint);
 
         if (mAttachInfo.mContentCaptureManager != null) {
diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java
index 4a79ba6..febdac2 100644
--- a/core/java/android/view/inputmethod/EditorInfo.java
+++ b/core/java/android/view/inputmethod/EditorInfo.java
@@ -595,6 +595,10 @@
                 == HandwritingGesture.GESTURE_TYPE_SELECT) {
             list.add(SelectGesture.class);
         }
+        if ((mSupportedHandwritingGestureTypes & HandwritingGesture.GESTURE_TYPE_SELECT_RANGE)
+                == HandwritingGesture.GESTURE_TYPE_SELECT_RANGE) {
+            list.add(SelectRangeGesture.class);
+        }
         if ((mSupportedHandwritingGestureTypes & HandwritingGesture.GESTURE_TYPE_INSERT)
                 == HandwritingGesture.GESTURE_TYPE_INSERT) {
             list.add(InsertGesture.class);
@@ -603,6 +607,10 @@
                 == HandwritingGesture.GESTURE_TYPE_DELETE) {
             list.add(DeleteGesture.class);
         }
+        if ((mSupportedHandwritingGestureTypes & HandwritingGesture.GESTURE_TYPE_DELETE_RANGE)
+                == HandwritingGesture.GESTURE_TYPE_DELETE_RANGE) {
+            list.add(DeleteRangeGesture.class);
+        }
         if ((mSupportedHandwritingGestureTypes & HandwritingGesture.GESTURE_TYPE_REMOVE_SPACE)
                 == HandwritingGesture.GESTURE_TYPE_REMOVE_SPACE) {
             list.add(RemoveSpaceGesture.class);
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 18b3e21..201efe8 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -776,20 +776,20 @@
                     "InputMethodManager.DelegateImpl#startInputAsyncOnWindowFocusGain",
                     InputMethodManager.this, null /* icProto */);
 
-            final ImeFocusController controller = getFocusController();
-            if (controller == null) {
-                return;
-            }
-
+            boolean checkFocusResult;
             synchronized (mH) {
+                if (mCurRootView == null) {
+                    return;
+                }
                 if (mRestartOnNextWindowFocus) {
                     if (DEBUG) Log.v(TAG, "Restarting due to mRestartOnNextWindowFocus as true");
                     mRestartOnNextWindowFocus = false;
                     forceNewFocus = true;
                 }
+                checkFocusResult = checkFocusInternalLocked(forceNewFocus, mCurRootView);
             }
 
-            if (controller.checkFocus(forceNewFocus, false)) {
+            if (checkFocusResult) {
                 // We need to restart input on the current focus view.  This
                 // should be done in conjunction with telling the system service
                 // about the window gaining focus, to help make the transition
@@ -825,9 +825,15 @@
         }
 
         @Override
-        public boolean checkFocus(boolean forceNewFocus, boolean startInput,
-                ViewRootImpl viewRootImpl) {
-            return checkFocusInternal(forceNewFocus, startInput, viewRootImpl);
+        public void onScheduledCheckFocus(ViewRootImpl viewRootImpl) {
+            synchronized (mH) {
+                if (!checkFocusInternalLocked(false, viewRootImpl)) {
+                    return;
+                }
+            }
+            startInputOnWindowFocusGainInternal(StartInputReason.SCHEDULED_CHECK_FOCUS,
+                    null /* focusedView */, 0 /* startInputFlags */, 0 /* softInputMode */,
+                    0 /* windowFlags */);
         }
 
         @Override
@@ -937,15 +943,6 @@
         return mCurRootView != null ? mNextServedView : null;
     }
 
-    private ImeFocusController getFocusController() {
-        synchronized (mH) {
-            if (mCurRootView != null) {
-                return mCurRootView.getImeFocusController();
-            }
-            return null;
-        }
-    }
-
     /**
      * Returns {@code true} when the given view has been served by Input Method.
      */
@@ -1128,8 +1125,7 @@
                         if (mCurRootView == null) {
                             return;
                         }
-                        if (!mCurRootView.getImeFocusController().checkFocus(
-                                mRestartOnNextWindowFocus, false)) {
+                        if (!checkFocusInternalLocked(mRestartOnNextWindowFocus, mCurRootView)) {
                             return;
                         }
                         final int reason = active ? StartInputReason.ACTIVATED_BY_IMMS
@@ -2349,8 +2345,7 @@
     }
 
     /**
-     * Called from {@link #checkFocusInternal(boolean, boolean, ViewRootImpl)},
-     * {@link #restartInput(View)}, {@link #MSG_BIND} or {@link #MSG_UNBIND}.
+     * Starts an input connection from the served view that gains the window focus.
      * Note that this method should *NOT* be called inside of {@code mH} lock to prevent start input
      * background thread may blocked by other methods which already inside {@code mH} lock.
      */
@@ -2658,52 +2653,53 @@
     }
 
     /**
-     * Check the next served view from {@link ImeFocusController} if needs to start input.
      * Note that this method should *NOT* be called inside of {@code mH} lock to prevent start input
      * background thread may blocked by other methods which already inside {@code mH} lock.
      * @hide
      */
     @UnsupportedAppUsage
     public void checkFocus() {
-        final ImeFocusController controller = getFocusController();
-        if (controller != null) {
-            controller.checkFocus(false /* forceNewFocus */, true /* startInput */);
+        synchronized (mH) {
+            if (mCurRootView == null) {
+                return;
+            }
+            if (!checkFocusInternalLocked(false /* forceNewFocus */, mCurRootView)) {
+                return;
+            }
         }
+        startInputOnWindowFocusGainInternal(StartInputReason.CHECK_FOCUS,
+                null /* focusedView */,
+                0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */);
     }
 
-    private boolean checkFocusInternal(boolean forceNewFocus, boolean startInput,
-            ViewRootImpl viewRootImpl) {
-        synchronized (mH) {
-            if (mCurRootView != viewRootImpl) {
-                return false;
-            }
-            if (mServedView == mNextServedView && !forceNewFocus) {
-                return false;
-            }
-            if (DEBUG) {
-                Log.v(TAG, "checkFocus: view=" + mServedView
-                        + " next=" + mNextServedView
-                        + " force=" + forceNewFocus
-                        + " package="
-                        + (mServedView != null ? mServedView.getContext().getPackageName()
-                        : "<none>"));
-            }
-            // Close the connection when no next served view coming.
-            if (mNextServedView == null) {
-                finishInputLocked();
-                closeCurrentInput();
-                return false;
-            }
-            mServedView = mNextServedView;
-            if (mServedInputConnection != null) {
-                mServedInputConnection.finishComposingTextFromImm();
-            }
+    /**
+     * Check the next served view if needs to start input.
+     */
+    @GuardedBy("mH")
+    private boolean checkFocusInternalLocked(boolean forceNewFocus, ViewRootImpl viewRootImpl) {
+        if (mCurRootView != viewRootImpl) {
+            return false;
         }
-
-        if (startInput) {
-            startInputOnWindowFocusGainInternal(StartInputReason.CHECK_FOCUS,
-                    null /* focusedView */,
-                    0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */);
+        if (mServedView == mNextServedView && !forceNewFocus) {
+            return false;
+        }
+        if (DEBUG) {
+            Log.v(TAG, "checkFocus: view=" + mServedView
+                    + " next=" + mNextServedView
+                    + " force=" + forceNewFocus
+                    + " package="
+                    + (mServedView != null ? mServedView.getContext().getPackageName()
+                    : "<none>"));
+        }
+        // Close the connection when no next served view coming.
+        if (mNextServedView == null) {
+            finishInputLocked();
+            closeCurrentInput();
+            return false;
+        }
+        mServedView = mNextServedView;
+        if (mServedInputConnection != null) {
+            mServedInputConnection.finishComposingTextFromImm();
         }
         return true;
     }
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index a2e9faa..57103e4 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -189,6 +189,7 @@
 import android.view.inputmethod.CorrectionInfo;
 import android.view.inputmethod.CursorAnchorInfo;
 import android.view.inputmethod.DeleteGesture;
+import android.view.inputmethod.DeleteRangeGesture;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
@@ -199,6 +200,7 @@
 import android.view.inputmethod.JoinOrSplitGesture;
 import android.view.inputmethod.RemoveSpaceGesture;
 import android.view.inputmethod.SelectGesture;
+import android.view.inputmethod.SelectRangeGesture;
 import android.view.inspector.InspectableProperty;
 import android.view.inspector.InspectableProperty.EnumEntry;
 import android.view.inspector.InspectableProperty.FlagEntry;
@@ -9096,7 +9098,9 @@
 
                 ArrayList<Class<? extends HandwritingGesture>> gestures = new ArrayList<>();
                 gestures.add(SelectGesture.class);
+                gestures.add(SelectRangeGesture.class);
                 gestures.add(DeleteGesture.class);
+                gestures.add(DeleteRangeGesture.class);
                 gestures.add(InsertGesture.class);
                 gestures.add(RemoveSpaceGesture.class);
                 gestures.add(JoinOrSplitGesture.class);
@@ -9325,6 +9329,26 @@
     }
 
     /** @hide */
+    public int performHandwritingSelectRangeGesture(@NonNull SelectRangeGesture gesture) {
+        Range<Integer> startRange = getRangeForRect(
+                convertFromScreenToContentCoordinates(gesture.getSelectionStartArea()),
+                gesture.getGranularity());
+        if (startRange == null) {
+            return handleGestureFailure(gesture);
+        }
+        Range<Integer> endRange = getRangeForRect(
+                convertFromScreenToContentCoordinates(gesture.getSelectionEndArea()),
+                gesture.getGranularity());
+        if (endRange == null || endRange.getUpper() <= startRange.getLower()) {
+            return handleGestureFailure(gesture);
+        }
+        Range<Integer> range = startRange.extend(endRange);
+        Selection.setSelection(getEditableText(), range.getLower(), range.getUpper());
+        mEditor.startSelectionActionModeAsync(/* adjustSelection= */ false);
+        return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
+    }
+
+    /** @hide */
     public int performHandwritingDeleteGesture(@NonNull DeleteGesture gesture) {
         Range<Integer> range = getRangeForRect(
                 convertFromScreenToContentCoordinates(gesture.getDeletionArea()),
@@ -9332,60 +9356,99 @@
         if (range == null) {
             return handleGestureFailure(gesture);
         }
+
+        if (gesture.getGranularity() == HandwritingGesture.GRANULARITY_WORD) {
+            range = adjustHandwritingDeleteGestureRange(range);
+        }
+
+        getEditableText().delete(range.getLower(), range.getUpper());
+        Selection.setSelection(getEditableText(), range.getLower());
+        return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
+    }
+
+    /** @hide */
+    public int performHandwritingDeleteRangeGesture(@NonNull DeleteRangeGesture gesture) {
+        Range<Integer> startRange = getRangeForRect(
+                convertFromScreenToContentCoordinates(gesture.getDeletionStartArea()),
+                gesture.getGranularity());
+        if (startRange == null) {
+            return handleGestureFailure(gesture);
+        }
+        Range<Integer> endRange = getRangeForRect(
+                convertFromScreenToContentCoordinates(gesture.getDeletionEndArea()),
+                gesture.getGranularity());
+        if (endRange == null) {
+            return handleGestureFailure(gesture);
+        }
+        Range<Integer> range = startRange.extend(endRange);
+
+        if (gesture.getGranularity() == HandwritingGesture.GRANULARITY_WORD) {
+            range = adjustHandwritingDeleteGestureRange(range);
+        }
+
+        getEditableText().delete(range.getLower(), range.getUpper());
+        Selection.setSelection(getEditableText(), range.getLower());
+        return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
+    }
+
+    private Range<Integer> adjustHandwritingDeleteGestureRange(Range<Integer> range) {
+        // For handwriting delete gestures with word granularity, adjust the start and end offsets
+        // to remove extra whitespace around the deleted text.
+
         int start = range.getLower();
         int end = range.getUpper();
 
-        // For word granularity, adjust the start and end offsets to remove extra whitespace around
-        // the deleted text.
-        if (gesture.getGranularity() == HandwritingGesture.GRANULARITY_WORD) {
-            // If the deleted text is at the start of the text, the behavior is the same as the case
-            // where the deleted text follows a new line character.
-            int codePointBeforeStart = start > 0
-                    ? Character.codePointBefore(mText, start) : TextUtils.LINE_FEED_CODE_POINT;
-            // If the deleted text is at the end of the text, the behavior is the same as the case
-            // where the deleted text precedes a new line character.
-            int codePointAtEnd = end < mText.length()
-                    ? Character.codePointAt(mText, end) : TextUtils.LINE_FEED_CODE_POINT;
-            if (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart)
-                    && (TextUtils.isWhitespace(codePointAtEnd)
-                            || TextUtils.isPunctuation(codePointAtEnd))) {
-                // Remove whitespace (except new lines) before the deleted text, in these cases:
-                // - There is whitespace following the deleted text
-                //     e.g. "one [deleted] three" -> "one | three" -> "one| three"
-                // - There is punctuation following the deleted text
-                //     e.g. "one [deleted]!" -> "one |!" -> "one|!"
-                // - There is a new line following the deleted text
-                //     e.g. "one [deleted]\n" -> "one |\n" -> "one|\n"
-                // - The deleted text is at the end of the text
-                //     e.g. "one [deleted]" -> "one |" -> "one|"
-                // (The pipe | indicates the cursor position.)
-                do {
-                    start -= Character.charCount(codePointBeforeStart);
-                    if (start == 0) break;
-                    codePointBeforeStart = Character.codePointBefore(mText, start);
-                } while (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart));
-            } else if (TextUtils.isWhitespaceExceptNewline(codePointAtEnd)
-                    && (TextUtils.isWhitespace(codePointBeforeStart)
-                            || TextUtils.isPunctuation(codePointBeforeStart))) {
-                // Remove whitespace (except new lines) after the deleted text, in these cases:
-                // - There is punctuation preceding the deleted text
-                //     e.g. "([deleted] two)" -> "(| two)" -> "(|two)"
-                // - There is a new line preceding the deleted text
-                //     e.g. "\n[deleted] two" -> "\n| two" -> "\n|two"
-                // - The deleted text is at the start of the text
-                //     e.g. "[deleted] two" -> "| two" -> "|two"
-                // (The pipe | indicates the cursor position.)
-                do {
-                    end += Character.charCount(codePointAtEnd);
-                    if (end == mText.length()) break;
-                    codePointAtEnd = Character.codePointAt(mText, end);
-                } while (TextUtils.isWhitespaceExceptNewline(codePointAtEnd));
-            }
+        // If the deleted text is at the start of the text, the behavior is the same as the case
+        // where the deleted text follows a new line character.
+        int codePointBeforeStart = start > 0
+                ? Character.codePointBefore(mText, start) : TextUtils.LINE_FEED_CODE_POINT;
+        // If the deleted text is at the end of the text, the behavior is the same as the case where
+        // the deleted text precedes a new line character.
+        int codePointAtEnd = end < mText.length()
+                ? Character.codePointAt(mText, end) : TextUtils.LINE_FEED_CODE_POINT;
+
+        if (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart)
+                && (TextUtils.isWhitespace(codePointAtEnd)
+                        || TextUtils.isPunctuation(codePointAtEnd))) {
+            // Remove whitespace (except new lines) before the deleted text, in these cases:
+            // - There is whitespace following the deleted text
+            //     e.g. "one [deleted] three" -> "one | three" -> "one| three"
+            // - There is punctuation following the deleted text
+            //     e.g. "one [deleted]!" -> "one |!" -> "one|!"
+            // - There is a new line following the deleted text
+            //     e.g. "one [deleted]\n" -> "one |\n" -> "one|\n"
+            // - The deleted text is at the end of the text
+            //     e.g. "one [deleted]" -> "one |" -> "one|"
+            // (The pipe | indicates the cursor position.)
+            do {
+                start -= Character.charCount(codePointBeforeStart);
+                if (start == 0) break;
+                codePointBeforeStart = Character.codePointBefore(mText, start);
+            } while (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart));
+            return new Range(start, end);
         }
 
-        getEditableText().delete(start, end);
-        Selection.setSelection(getEditableText(), start);
-        return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
+        if (TextUtils.isWhitespaceExceptNewline(codePointAtEnd)
+                && (TextUtils.isWhitespace(codePointBeforeStart)
+                        || TextUtils.isPunctuation(codePointBeforeStart))) {
+            // Remove whitespace (except new lines) after the deleted text, in these cases:
+            // - There is punctuation preceding the deleted text
+            //     e.g. "([deleted] two)" -> "(| two)" -> "(|two)"
+            // - There is a new line preceding the deleted text
+            //     e.g. "\n[deleted] two" -> "\n| two" -> "\n|two"
+            // - The deleted text is at the start of the text
+            //     e.g. "[deleted] two" -> "| two" -> "|two"
+            // (The pipe | indicates the cursor position.)
+            do {
+                end += Character.charCount(codePointAtEnd);
+                if (end == mText.length()) break;
+                codePointAtEnd = Character.codePointAt(mText, end);
+            } while (TextUtils.isWhitespaceExceptNewline(codePointAtEnd));
+            return new Range(start, end);
+        }
+
+        // Return the original range.
+        return range;
     }
 
     /** @hide */
diff --git a/core/java/com/android/internal/inputmethod/EditableInputConnection.java b/core/java/com/android/internal/inputmethod/EditableInputConnection.java
index f260d7d..f600c36 100644
--- a/core/java/com/android/internal/inputmethod/EditableInputConnection.java
+++ b/core/java/com/android/internal/inputmethod/EditableInputConnection.java
@@ -35,6 +35,7 @@
 import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.CorrectionInfo;
 import android.view.inputmethod.DeleteGesture;
+import android.view.inputmethod.DeleteRangeGesture;
 import android.view.inputmethod.DumpableInputConnection;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
@@ -44,6 +45,7 @@
 import android.view.inputmethod.JoinOrSplitGesture;
 import android.view.inputmethod.RemoveSpaceGesture;
 import android.view.inputmethod.SelectGesture;
+import android.view.inputmethod.SelectRangeGesture;
 import android.widget.TextView;
 
 import java.util.concurrent.Executor;
@@ -275,8 +277,12 @@
         int result;
         if (gesture instanceof SelectGesture) {
             result = mTextView.performHandwritingSelectGesture((SelectGesture) gesture);
+        } else if (gesture instanceof SelectRangeGesture) {
+            result = mTextView.performHandwritingSelectRangeGesture((SelectRangeGesture) gesture);
         } else if (gesture instanceof DeleteGesture) {
             result = mTextView.performHandwritingDeleteGesture((DeleteGesture) gesture);
+        } else if (gesture instanceof DeleteRangeGesture) {
+            result = mTextView.performHandwritingDeleteRangeGesture((DeleteRangeGesture) gesture);
         } else if (gesture instanceof InsertGesture) {
             result = mTextView.performHandwritingInsertGesture((InsertGesture) gesture);
         } else if (gesture instanceof RemoveSpaceGesture) {
diff --git a/core/java/com/android/internal/inputmethod/InputMethodDebug.java b/core/java/com/android/internal/inputmethod/InputMethodDebug.java
index 09c97b3..1b4afd6 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodDebug.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodDebug.java
@@ -49,6 +49,8 @@
                 return "WINDOW_FOCUS_GAIN";
             case StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY:
                 return "WINDOW_FOCUS_GAIN_REPORT_ONLY";
+            case StartInputReason.SCHEDULED_CHECK_FOCUS:
+                return "SCHEDULED_CHECK_FOCUS";
             case StartInputReason.APP_CALLED_RESTART_INPUT_API:
                 return "APP_CALLED_RESTART_INPUT_API";
             case StartInputReason.CHECK_FOCUS:
diff --git a/core/java/com/android/internal/inputmethod/StartInputReason.java b/core/java/com/android/internal/inputmethod/StartInputReason.java
index 51ed841..733d975 100644
--- a/core/java/com/android/internal/inputmethod/StartInputReason.java
+++ b/core/java/com/android/internal/inputmethod/StartInputReason.java
@@ -31,6 +31,7 @@
         StartInputReason.UNSPECIFIED,
         StartInputReason.WINDOW_FOCUS_GAIN,
         StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY,
+        StartInputReason.SCHEDULED_CHECK_FOCUS,
         StartInputReason.APP_CALLED_RESTART_INPUT_API,
         StartInputReason.CHECK_FOCUS,
         StartInputReason.BOUND_TO_IMMS,
@@ -58,6 +59,11 @@
      */
     int WINDOW_FOCUS_GAIN_REPORT_ONLY = 2;
     /**
+     * Similar to {@link #CHECK_FOCUS}, but the one scheduled with
+     * {@link android.view.ViewRootImpl#dispatchCheckFocus()}.
+     */
+    int SCHEDULED_CHECK_FOCUS = 3;
+    /**
      * {@link android.view.inputmethod.InputMethodManager#restartInput(android.view.View)} is
      * either explicitly called by the application or indirectly called by some Framework class
      * (e.g. {@link android.widget.EditText}).
diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml
index a7f2aa7..be1c939 100644
--- a/core/res/res/layout/notification_template_header.xml
+++ b/core/res/res/layout/notification_template_header.xml
@@ -24,6 +24,7 @@
     android:gravity="center_vertical"
     android:orientation="horizontal"
     android:theme="@style/Theme.DeviceDefault.Notification"
+    android:importantForAccessibility="no"
     >
 
     <ImageView
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index b5cdcff..caa67de 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3559,9 +3559,9 @@
          config_sidefpsSkipWaitForPowerVendorAcquireMessage -->
     <integer name="config_sidefpsSkipWaitForPowerAcquireMessage">6</integer>
 
-    <!-- This vendor acquired message that will cause the sidefpsKgPowerPress window to be skipped.
-         config_sidefpsSkipWaitForPowerOnFingerUp must be true and
-         config_sidefpsSkipWaitForPowerAcquireMessage must be BIOMETRIC_ACQUIRED_VENDOR == 6. -->
+    <!-- This vendor acquired message will cause the sidefpsKgPowerPress window to be skipped
+         when config_sidefpsSkipWaitForPowerAcquireMessage == 6 (VENDOR) and the vendor acquire
+         message equals this constant -->
     <integer name="config_sidefpsSkipWaitForPowerVendorAcquireMessage">2</integer>
 
     <!-- This config is used to force VoiceInteractionService to start on certain low ram devices.
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index a1d73ff..71b2f00 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -117,4 +117,17 @@
     <!-- Whether using the new SubscriptionManagerService or the old SubscriptionController -->
     <bool name="config_using_subscription_manager_service">false</bool>
     <java-symbol type="bool" name="config_using_subscription_manager_service" />
+
+    <!-- Boolean indicating whether the emergency numbers for a country, sourced from modem/config,
+         should be ignored if that country is 'locked' (i.e. ignore_modem_config set to true) in
+         Android Emergency DB. If this value is true, emergency numbers for a country, sourced from
+         modem/config, will be ignored if that country is 'locked' in Android Emergency DB. -->
+    <bool name="ignore_modem_config_emergency_numbers">false</bool>
+    <java-symbol type="bool" name="ignore_modem_config_emergency_numbers" />
+
+    <!-- Boolean indicating whether emergency numbers routing from the android emergency number
+         database should be ignored (i.e. routing will always be set to UNKNOWN). If this value is
+         true, routing from the android emergency number database will be ignored. -->
+    <bool name="ignore_emergency_number_routing_from_db">false</bool>
+    <java-symbol type="bool" name="ignore_emergency_number_routing_from_db" />
 </resources>
diff --git a/core/tests/BroadcastRadioTests/Android.bp b/core/tests/BroadcastRadioTests/Android.bp
index 113f45d..7cb64c8 100644
--- a/core/tests/BroadcastRadioTests/Android.bp
+++ b/core/tests/BroadcastRadioTests/Android.bp
@@ -23,23 +23,32 @@
 
 android_test {
     name: "BroadcastRadioTests",
+    srcs: ["src/**/*.java"],
     privileged: true,
     certificate: "platform",
     // TODO(b/13282254): uncomment when b/13282254 is fixed
     // sdk_version: "current"
     platform_apis: true,
-    static_libs: [
-        "compatibility-device-util-axt",
-        "androidx.test.rules",
-        "testng",
-        "services.core",
-    ],
-    libs: ["android.test.base"],
-    srcs: ["src/**/*.java"],
     dex_preopt: {
         enabled: false,
     },
     optimize: {
         enabled: false,
     },
+    static_libs: [
+        "services.core",
+        "androidx.test.rules",
+        "truth-prebuilt",
+        "testng",
+        "mockito-target-extended",
+    ],
+    libs: ["android.test.base"],
+    test_suites: [
+        "general-tests",
+    ],
+    // mockito-target-inline dependency
+    jni_libs: [
+        "libcarservicejni",
+        "libdexmakerjvmtiagent",
+    ],
 }
diff --git a/core/tests/BroadcastRadioTests/AndroidManifest.xml b/core/tests/BroadcastRadioTests/AndroidManifest.xml
index ce12cc9..869b484 100644
--- a/core/tests/BroadcastRadioTests/AndroidManifest.xml
+++ b/core/tests/BroadcastRadioTests/AndroidManifest.xml
@@ -19,7 +19,7 @@
 
     <uses-permission android:name="android.permission.ACCESS_BROADCAST_RADIO" />
 
-    <application>
+    <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
     </application>
 
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
index 11eb158..3f35e99 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
@@ -33,11 +33,9 @@
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioTuner;
-import android.test.suitebuilder.annotation.MediumTest;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.Before;
@@ -47,6 +45,7 @@
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnitRunner;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -56,8 +55,7 @@
 /**
  * A test for broadcast radio API.
  */
-@RunWith(AndroidJUnit4.class)
-@MediumTest
+@RunWith(MockitoJUnitRunner.class)
 public class RadioTunerTest {
     private static final String TAG = "BroadcastRadioTests.RadioTuner";
 
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
new file mode 100644
index 0000000..259a118
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
@@ -0,0 +1,490 @@
+/*
+ * 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 android.hardware.radio.tests.unittests;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+
+import org.junit.Test;
+
+public final class RadioManagerTest {
+
+    private static final int REGION = RadioManager.REGION_ITU_2;
+    private static final int FM_LOWER_LIMIT = 87500;
+    private static final int FM_UPPER_LIMIT = 108000;
+    private static final int FM_SPACING = 200;
+    private static final int AM_LOWER_LIMIT = 540;
+    private static final int AM_UPPER_LIMIT = 1700;
+    private static final int AM_SPACING = 10;
+    private static final boolean STEREO_SUPPORTED = true;
+    private static final boolean RDS_SUPPORTED = true;
+    private static final boolean TA_SUPPORTED = false;
+    private static final boolean AF_SUPPORTED = false;
+    private static final boolean EA_SUPPORTED = false;
+
+    private static final int PROPERTIES_ID = 10;
+    private static final String SERVICE_NAME = "ServiceNameMock";
+    private static final int CLASS_ID = RadioManager.CLASS_AM_FM;
+    private static final String IMPLEMENTOR = "ImplementorMock";
+    private static final String PRODUCT = "ProductMock";
+    private static final String VERSION = "VersionMock";
+    private static final String SERIAL = "SerialMock";
+    private static final int NUM_TUNERS = 1;
+    private static final int NUM_AUDIO_SOURCES = 1;
+    private static final boolean IS_INITIALIZATION_REQUIRED = false;
+    private static final boolean IS_CAPTURE_SUPPORTED = false;
+    private static final boolean IS_BG_SCAN_SUPPORTED = true;
+    private static final int[] SUPPORTED_PROGRAM_TYPES = new int[]{
+            ProgramSelector.PROGRAM_TYPE_AM, ProgramSelector.PROGRAM_TYPE_FM};
+    private static final int[] SUPPORTED_IDENTIFIERS_TYPES = new int[]{
+            ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, ProgramSelector.IDENTIFIER_TYPE_RDS_PI};
+
+    private static final RadioManager.FmBandDescriptor FM_BAND_DESCRIPTOR =
+            createFmBandDescriptor();
+    private static final RadioManager.AmBandDescriptor AM_BAND_DESCRIPTOR =
+            createAmBandDescriptor();
+    private static final RadioManager.FmBandConfig FM_BAND_CONFIG = createFmBandConfig();
+    private static final RadioManager.AmBandConfig AM_BAND_CONFIG = createAmBandConfig();
+    private static final RadioManager.ModuleProperties AMFM_PROPERTIES = createAmFmProperties();
+
+    @Test
+    public void getType_forBandDescriptor() {
+        RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor();
+
+        assertWithMessage("AM Band Descriptor type")
+                .that(bandDescriptor.getType()).isEqualTo(RadioManager.BAND_AM);
+    }
+
+    @Test
+    public void getRegion_forBandDescriptor() {
+        RadioManager.BandDescriptor bandDescriptor = createFmBandDescriptor();
+
+        assertWithMessage("FM Band Descriptor region")
+                .that(bandDescriptor.getRegion()).isEqualTo(REGION);
+    }
+
+    @Test
+    public void getLowerLimit_forBandDescriptor() {
+        RadioManager.BandDescriptor bandDescriptor = createFmBandDescriptor();
+
+        assertWithMessage("FM Band Descriptor lower limit")
+                .that(bandDescriptor.getLowerLimit()).isEqualTo(FM_LOWER_LIMIT);
+    }
+
+    @Test
+    public void getUpperLimit_forBandDescriptor() {
+        RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor();
+
+        assertWithMessage("AM Band Descriptor upper limit")
+                .that(bandDescriptor.getUpperLimit()).isEqualTo(AM_UPPER_LIMIT);
+    }
+
+    @Test
+    public void getSpacing_forBandDescriptor() {
+        RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor();
+
+        assertWithMessage("AM Band Descriptor spacing")
+                .that(bandDescriptor.getSpacing()).isEqualTo(AM_SPACING);
+    }
+
+    @Test
+    public void isAmBand_forAmBandDescriptor_returnsTrue() {
+        RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor();
+
+        assertWithMessage("Is AM Band Descriptor an AM band")
+                .that(bandDescriptor.isAmBand()).isTrue();
+    }
+
+    @Test
+    public void isFmBand_forAmBandDescriptor_returnsFalse() {
+        RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor();
+
+        assertWithMessage("Is AM Band Descriptor an FM band")
+                .that(bandDescriptor.isFmBand()).isFalse();
+    }
+
+    @Test
+    public void isStereoSupported_forFmBandDescriptor() {
+        assertWithMessage("FM Band Descriptor stereo")
+                .that(FM_BAND_DESCRIPTOR.isStereoSupported()).isEqualTo(STEREO_SUPPORTED);
+    }
+
+    @Test
+    public void isRdsSupported_forFmBandDescriptor() {
+        assertWithMessage("FM Band Descriptor RDS or RBDS")
+                .that(FM_BAND_DESCRIPTOR.isRdsSupported()).isEqualTo(RDS_SUPPORTED);
+    }
+
+    @Test
+    public void isTaSupported_forFmBandDescriptor() {
+        assertWithMessage("FM Band Descriptor traffic announcement")
+                .that(FM_BAND_DESCRIPTOR.isTaSupported()).isEqualTo(TA_SUPPORTED);
+    }
+
+    @Test
+    public void isAfSupported_forFmBandDescriptor() {
+        assertWithMessage("FM Band Descriptor alternate frequency")
+                .that(FM_BAND_DESCRIPTOR.isAfSupported()).isEqualTo(AF_SUPPORTED);
+    }
+
+    @Test
+    public void isEaSupported_forFmBandDescriptor() {
+        assertWithMessage("FM Band Descriptor emergency announcement")
+                .that(FM_BAND_DESCRIPTOR.isEaSupported()).isEqualTo(EA_SUPPORTED);
+    }
+
+    @Test
+    public void isStereoSupported_forAmBandDescriptor() {
+        assertWithMessage("AM Band Descriptor stereo")
+                .that(AM_BAND_DESCRIPTOR.isStereoSupported()).isEqualTo(STEREO_SUPPORTED);
+    }
+
+    @Test
+    public void equals_withSameFmBandDescriptors_returnsTrue() {
+        RadioManager.FmBandDescriptor fmBandDescriptor1 = createFmBandDescriptor();
+        RadioManager.FmBandDescriptor fmBandDescriptor2 = createFmBandDescriptor();
+
+        assertWithMessage("The same FM Band Descriptor")
+                .that(fmBandDescriptor1).isEqualTo(fmBandDescriptor2);
+    }
+
+    @Test
+    public void equals_withSameAmBandDescriptors_returnsTrue() {
+        RadioManager.AmBandDescriptor amBandDescriptorCompared = createAmBandDescriptor();
+
+        assertWithMessage("The same AM Band Descriptor")
+                .that(AM_BAND_DESCRIPTOR).isEqualTo(amBandDescriptorCompared);
+    }
+
+    @Test
+    public void equals_withAmBandDescriptorsOfDifferentUpperLimits_returnsFalse() {
+        RadioManager.AmBandDescriptor amBandDescriptorCompared =
+                new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM, AM_LOWER_LIMIT,
+                        AM_UPPER_LIMIT + AM_SPACING, AM_SPACING, STEREO_SUPPORTED);
+
+        assertWithMessage("AM Band Descriptor of different upper limit")
+                .that(AM_BAND_DESCRIPTOR).isNotEqualTo(amBandDescriptorCompared);
+    }
+
+    @Test
+    public void equals_withAmAndFmBandDescriptors_returnsFalse() {
+        assertWithMessage("AM Band Descriptor")
+                .that(AM_BAND_DESCRIPTOR).isNotEqualTo(FM_BAND_DESCRIPTOR);
+    }
+
+    @Test
+    public void getType_forBandConfig() {
+        RadioManager.BandConfig fmBandConfig = createFmBandConfig();
+
+        assertWithMessage("FM Band Config type")
+                .that(fmBandConfig.getType()).isEqualTo(RadioManager.BAND_FM);
+    }
+
+    @Test
+    public void getRegion_forBandConfig() {
+        RadioManager.BandConfig amBandConfig = createAmBandConfig();
+
+        assertWithMessage("AM Band Config region")
+                .that(amBandConfig.getRegion()).isEqualTo(REGION);
+    }
+
+    @Test
+    public void getLowerLimit_forBandConfig() {
+        RadioManager.BandConfig amBandConfig = createAmBandConfig();
+
+        assertWithMessage("AM Band Config lower limit")
+                .that(amBandConfig.getLowerLimit()).isEqualTo(AM_LOWER_LIMIT);
+    }
+
+    @Test
+    public void getUpperLimit_forBandConfig() {
+        RadioManager.BandConfig fmBandConfig = createFmBandConfig();
+
+        assertWithMessage("FM Band Config upper limit")
+                .that(fmBandConfig.getUpperLimit()).isEqualTo(FM_UPPER_LIMIT);
+    }
+
+    @Test
+    public void getSpacing_forBandConfig() {
+        RadioManager.BandConfig fmBandConfig = createFmBandConfig();
+
+        assertWithMessage("FM Band Config spacing")
+                .that(fmBandConfig.getSpacing()).isEqualTo(FM_SPACING);
+    }
+
+    @Test
+    public void getStereo_forFmBandConfig() {
+        assertWithMessage("FM Band Config stereo ")
+                .that(FM_BAND_CONFIG.getStereo()).isEqualTo(STEREO_SUPPORTED);
+    }
+
+    @Test
+    public void getRds_forFmBandConfig() {
+        assertWithMessage("FM Band Config RDS or RBDS")
+                .that(FM_BAND_CONFIG.getRds()).isEqualTo(RDS_SUPPORTED);
+    }
+
+    @Test
+    public void getTa_forFmBandConfig() {
+        assertWithMessage("FM Band Config traffic announcement")
+                .that(FM_BAND_CONFIG.getTa()).isEqualTo(TA_SUPPORTED);
+    }
+
+    @Test
+    public void getAf_forFmBandConfig() {
+        assertWithMessage("FM Band Config alternate frequency")
+                .that(FM_BAND_CONFIG.getAf()).isEqualTo(AF_SUPPORTED);
+    }
+
+    @Test
+    public void getEa_forFmBandConfig() {
+        assertWithMessage("FM Band Config emergency Announcement")
+                .that(FM_BAND_CONFIG.getEa()).isEqualTo(EA_SUPPORTED);
+    }
+
+    @Test
+    public void getStereo_forAmBandConfig() {
+        assertWithMessage("AM Band Config stereo")
+                .that(AM_BAND_CONFIG.getStereo()).isEqualTo(STEREO_SUPPORTED);
+    }
+
+    @Test
+    public void equals_withSameFmBandConfigs_returnsTrue() {
+        RadioManager.FmBandConfig fmBandConfigCompared = createFmBandConfig();
+
+        assertWithMessage("The same FM Band Config")
+                .that(FM_BAND_CONFIG).isEqualTo(fmBandConfigCompared);
+    }
+
+    @Test
+    public void equals_withFmBandConfigsOfDifferentAfs_returnsFalse() {
+        RadioManager.FmBandConfig.Builder builder = new RadioManager.FmBandConfig.Builder(
+                createFmBandDescriptor()).setStereo(STEREO_SUPPORTED).setRds(RDS_SUPPORTED)
+                .setTa(TA_SUPPORTED).setAf(!AF_SUPPORTED).setEa(EA_SUPPORTED);
+        RadioManager.FmBandConfig fmBandConfigFromBuilder = builder.build();
+
+        assertWithMessage("FM Band Config of different af value")
+                .that(FM_BAND_CONFIG).isNotEqualTo(fmBandConfigFromBuilder);
+    }
+
+    @Test
+    public void equals_withFmAndAmBandConfigs_returnsFalse() {
+        assertWithMessage("FM Band Config")
+                .that(FM_BAND_CONFIG).isNotEqualTo(AM_BAND_CONFIG);
+    }
+
+    @Test
+    public void equals_withSameAmBandConfigs_returnsTrue() {
+        RadioManager.AmBandConfig amBandConfigCompared = createAmBandConfig();
+
+        assertWithMessage("The same AM Band Config")
+                .that(AM_BAND_CONFIG).isEqualTo(amBandConfigCompared);
+    }
+
+    @Test
+    public void equals_withAmBandConfigsOfDifferentTypes_returnsFalse() {
+        RadioManager.AmBandConfig amBandConfigCompared = new RadioManager.AmBandConfig(
+                new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM_HD, AM_LOWER_LIMIT,
+                        AM_UPPER_LIMIT, AM_SPACING, STEREO_SUPPORTED));
+
+        assertWithMessage("AM Band Config of different type")
+                .that(AM_BAND_CONFIG).isNotEqualTo(amBandConfigCompared);
+    }
+
+    @Test
+    public void equals_withAmBandConfigsOfDifferentStereoValues_returnsFalse() {
+        RadioManager.AmBandConfig.Builder builder = new RadioManager.AmBandConfig.Builder(
+                createAmBandDescriptor()).setStereo(!STEREO_SUPPORTED);
+        RadioManager.AmBandConfig amBandConfigFromBuilder = builder.build();
+
+        assertWithMessage("AM Band Config of different stereo value")
+                .that(AM_BAND_CONFIG).isNotEqualTo(amBandConfigFromBuilder);
+    }
+
+    @Test
+    public void getId_forModuleProperties() {
+        assertWithMessage("Properties id")
+                .that(AMFM_PROPERTIES.getId()).isEqualTo(PROPERTIES_ID);
+    }
+
+    @Test
+    public void getServiceName_forModuleProperties() {
+        assertWithMessage("Properties service name")
+                .that(AMFM_PROPERTIES.getServiceName()).isEqualTo(SERVICE_NAME);
+    }
+
+    @Test
+    public void getClassId_forModuleProperties() {
+        assertWithMessage("Properties class ID")
+                .that(AMFM_PROPERTIES.getClassId()).isEqualTo(CLASS_ID);
+    }
+
+    @Test
+    public void getImplementor_forModuleProperties() {
+        assertWithMessage("Properties implementor")
+                .that(AMFM_PROPERTIES.getImplementor()).isEqualTo(IMPLEMENTOR);
+    }
+
+    @Test
+    public void getProduct_forModuleProperties() {
+        assertWithMessage("Properties product")
+                .that(AMFM_PROPERTIES.getProduct()).isEqualTo(PRODUCT);
+    }
+
+    @Test
+    public void getVersion_forModuleProperties() {
+        assertWithMessage("Properties version")
+                .that(AMFM_PROPERTIES.getVersion()).isEqualTo(VERSION);
+    }
+
+    @Test
+    public void getSerial_forModuleProperties() {
+        assertWithMessage("Serial properties")
+                .that(AMFM_PROPERTIES.getSerial()).isEqualTo(SERIAL);
+    }
+
+    @Test
+    public void getNumTuners_forModuleProperties() {
+        assertWithMessage("Number of tuners in properties")
+                .that(AMFM_PROPERTIES.getNumTuners()).isEqualTo(NUM_TUNERS);
+    }
+
+    @Test
+    public void getNumAudioSources_forModuleProperties() {
+        assertWithMessage("Number of audio sources in properties")
+                .that(AMFM_PROPERTIES.getNumAudioSources()).isEqualTo(NUM_AUDIO_SOURCES);
+    }
+
+    @Test
+    public void isInitializationRequired_forModuleProperties() {
+        assertWithMessage("Initialization required in properties")
+                .that(AMFM_PROPERTIES.isInitializationRequired())
+                .isEqualTo(IS_INITIALIZATION_REQUIRED);
+    }
+
+    @Test
+    public void isCaptureSupported_forModuleProperties() {
+        assertWithMessage("Capture support in properties")
+                .that(AMFM_PROPERTIES.isCaptureSupported()).isEqualTo(IS_CAPTURE_SUPPORTED);
+    }
+
+    @Test
+    public void isBackgroundScanningSupported_forModuleProperties() {
+        assertWithMessage("Background scan support in properties")
+                .that(AMFM_PROPERTIES.isBackgroundScanningSupported())
+                .isEqualTo(IS_BG_SCAN_SUPPORTED);
+    }
+
+    @Test
+    public void isProgramTypeSupported_withSupportedType_forModuleProperties() {
+        assertWithMessage("AM/FM frequency type radio support in properties")
+                .that(AMFM_PROPERTIES.isProgramTypeSupported(
+                        ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY))
+                .isTrue();
+    }
+
+    @Test
+    public void isProgramTypeSupported_withNonSupportedType_forModuleProperties() {
+        assertWithMessage("DAB frequency type radio support in properties")
+                .that(AMFM_PROPERTIES.isProgramTypeSupported(
+                        ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY)).isFalse();
+    }
+
+    @Test
+    public void isProgramIdentifierSupported_withSupportedIdentifier_forModuleProperties() {
+        assertWithMessage("AM/FM frequency identifier radio support in properties")
+                .that(AMFM_PROPERTIES.isProgramIdentifierSupported(
+                        ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)).isTrue();
+    }
+
+    @Test
+    public void isProgramIdentifierSupported_withNonSupportedIdentifier_forModuleProperties() {
+        assertWithMessage("DAB frequency identifier radio support in properties")
+                .that(AMFM_PROPERTIES.isProgramIdentifierSupported(
+                        ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY)).isFalse();
+    }
+
+    @Test
+    public void getDabFrequencyTable_forModuleProperties() {
+        assertWithMessage("Properties DAB frequency table")
+                .that(AMFM_PROPERTIES.getDabFrequencyTable()).isNull();
+    }
+
+    @Test
+    public void getVendorInfo_forModuleProperties() {
+        assertWithMessage("Properties vendor info")
+                .that(AMFM_PROPERTIES.getVendorInfo()).isEmpty();
+    }
+
+    @Test
+    public void getBands_forModuleProperties() {
+        assertWithMessage("Properties bands")
+                .that(AMFM_PROPERTIES.getBands()).asList()
+                .containsExactly(AM_BAND_DESCRIPTOR, FM_BAND_DESCRIPTOR);
+    }
+
+    @Test
+    public void equals_withSameProperties_returnsTrue() {
+        RadioManager.ModuleProperties propertiesCompared = createAmFmProperties();
+
+        assertWithMessage("The same module properties")
+                .that(AMFM_PROPERTIES).isEqualTo(propertiesCompared);
+    }
+
+    @Test
+    public void equals_withModulePropertiesOfDifferentIds_returnsFalse() {
+        RadioManager.ModuleProperties propertiesDab = new RadioManager.ModuleProperties(
+                PROPERTIES_ID + 1, SERVICE_NAME, CLASS_ID, IMPLEMENTOR, PRODUCT, VERSION,
+                SERIAL, NUM_TUNERS, NUM_AUDIO_SOURCES, IS_INITIALIZATION_REQUIRED,
+                IS_CAPTURE_SUPPORTED, /* bands= */ null, IS_BG_SCAN_SUPPORTED,
+                SUPPORTED_PROGRAM_TYPES, SUPPORTED_IDENTIFIERS_TYPES, /* dabFrequencyTable= */ null,
+                /* vendorInfo= */ null);
+
+        assertWithMessage("Module properties of different id")
+                .that(AMFM_PROPERTIES).isNotEqualTo(propertiesDab);
+    }
+
+    private static RadioManager.ModuleProperties createAmFmProperties() {
+        return new RadioManager.ModuleProperties(PROPERTIES_ID, SERVICE_NAME, CLASS_ID,
+                IMPLEMENTOR, PRODUCT, VERSION, SERIAL, NUM_TUNERS, NUM_AUDIO_SOURCES,
+                IS_INITIALIZATION_REQUIRED, IS_CAPTURE_SUPPORTED,
+                new RadioManager.BandDescriptor[]{AM_BAND_DESCRIPTOR, FM_BAND_DESCRIPTOR},
+                IS_BG_SCAN_SUPPORTED, SUPPORTED_PROGRAM_TYPES, SUPPORTED_IDENTIFIERS_TYPES,
+                /* dabFrequencyTable= */ null, /* vendorInfo= */ null);
+    }
+
+    private static RadioManager.FmBandDescriptor createFmBandDescriptor() {
+        return new RadioManager.FmBandDescriptor(REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT,
+                FM_UPPER_LIMIT, FM_SPACING, STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED,
+                AF_SUPPORTED, EA_SUPPORTED);
+    }
+
+    private static RadioManager.AmBandDescriptor createAmBandDescriptor() {
+        return new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM, AM_LOWER_LIMIT,
+                AM_UPPER_LIMIT, AM_SPACING, STEREO_SUPPORTED);
+    }
+
+    private static RadioManager.FmBandConfig createFmBandConfig() {
+        return new RadioManager.FmBandConfig(createFmBandDescriptor());
+    }
+
+    private static RadioManager.AmBandConfig createAmBandConfig() {
+        return new RadioManager.AmBandConfig(createAmBandDescriptor());
+    }
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java
new file mode 100644
index 0000000..7f4ea11
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.broadcastradio;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.RadioManager;
+
+import com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+
+/**
+ * Tests for {@link android.hardware.radio.IRadioService} with AIDL HAL implementation
+ */
+@RunWith(MockitoJUnitRunner.class)
+public final class IRadioServiceAidlImplTest {
+
+    private static final int[] ENABLE_TYPES = new int[]{Announcement.TYPE_TRAFFIC};
+
+    private IRadioServiceAidlImpl mAidlImpl;
+
+    @Mock
+    private BroadcastRadioService mServiceMock;
+    @Mock
+    private BroadcastRadioServiceImpl mHalMock;
+    @Mock
+    private RadioManager.ModuleProperties mModuleMock;
+    @Mock
+    private RadioManager.BandConfig mBandConfigMock;
+    @Mock
+    private ITunerCallback mTunerCallbackMock;
+    @Mock
+    private IAnnouncementListener mListenerMock;
+    @Mock
+    private ICloseHandle mICloseHandle;
+    @Mock
+    private ITuner mTunerMock;
+
+    @Before
+    public void setUp() throws Exception {
+        doNothing().when(mServiceMock).enforcePolicyAccess();
+
+        when(mHalMock.listModules()).thenReturn(Arrays.asList(mModuleMock));
+        when(mHalMock.openSession(anyInt(), any(), anyBoolean(), any()))
+                .thenReturn(mTunerMock);
+        when(mHalMock.addAnnouncementListener(any(), any())).thenReturn(mICloseHandle);
+
+        mAidlImpl = new IRadioServiceAidlImpl(mServiceMock, mHalMock);
+    }
+
+    @Test
+    public void loadModules_forAidlImpl() {
+        assertWithMessage("Modules loaded in AIDL HAL")
+                .that(mAidlImpl.listModules())
+                .containsExactly(mModuleMock);
+    }
+
+    @Test
+    public void openTuner_forAidlImpl() throws Exception {
+        ITuner tuner = mAidlImpl.openTuner(/* moduleId= */ 0, mBandConfigMock,
+                /* withAudio= */ true, mTunerCallbackMock);
+
+        assertWithMessage("Tuner opened in AIDL HAL")
+                .that(tuner).isEqualTo(mTunerMock);
+    }
+
+    @Test
+    public void addAnnouncementListener_forAidlImpl() {
+        ICloseHandle closeHandle = mAidlImpl.addAnnouncementListener(ENABLE_TYPES, mListenerMock);
+
+        verify(mHalMock).addAnnouncementListener(ENABLE_TYPES, mListenerMock);
+        assertWithMessage("Close handle of announcement listener for HAL 2")
+                .that(closeHandle).isEqualTo(mICloseHandle);
+    }
+
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java
new file mode 100644
index 0000000..f28e27d
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.broadcastradio;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.RadioManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+
+/**
+ * Tests for {@link android.hardware.radio.IRadioService} with HIDL HAL implementation
+ */
+@RunWith(MockitoJUnitRunner.class)
+public final class IRadioServiceHidlImplTest {
+
+    private static final int HAL1_MODULE_ID = 0;
+    private static final int[] ENABLE_TYPES = new int[]{Announcement.TYPE_TRAFFIC};
+
+    private IRadioServiceHidlImpl mHidlImpl;
+
+    @Mock
+    private BroadcastRadioService mServiceMock;
+    @Mock
+    private com.android.server.broadcastradio.hal1.BroadcastRadioService mHal1Mock;
+    @Mock
+    private com.android.server.broadcastradio.hal2.BroadcastRadioService mHal2Mock;
+    @Mock
+    private RadioManager.ModuleProperties mHal1ModuleMock;
+    @Mock
+    private RadioManager.ModuleProperties mHal2ModuleMock;
+    @Mock
+    private RadioManager.BandConfig mBandConfigMock;
+    @Mock
+    private ITunerCallback mTunerCallbackMock;
+    @Mock
+    private IAnnouncementListener mListenerMock;
+    @Mock
+    private ICloseHandle mICloseHandle;
+    @Mock
+    private ITuner mHal1TunerMock;
+    @Mock
+    private ITuner mHal2TunerMock;
+
+    @Before
+    public void setup() throws Exception {
+        doNothing().when(mServiceMock).enforcePolicyAccess();
+        when(mHal1Mock.loadModules()).thenReturn(Arrays.asList(mHal1ModuleMock));
+        when(mHal1Mock.openTuner(anyInt(), any(), anyBoolean(), any())).thenReturn(mHal1TunerMock);
+
+        when(mHal2Mock.listModules()).thenReturn(Arrays.asList(mHal2ModuleMock));
+        doAnswer(invocation -> {
+            int moduleId = (int) invocation.getArguments()[0];
+            return moduleId != HAL1_MODULE_ID;
+        }).when(mHal2Mock).hasModule(anyInt());
+        when(mHal2Mock.openSession(anyInt(), any(), anyBoolean(), any()))
+                .thenReturn(mHal2TunerMock);
+        when(mHal2Mock.addAnnouncementListener(any(), any())).thenReturn(mICloseHandle);
+
+        mHidlImpl = new IRadioServiceHidlImpl(mServiceMock, mHal1Mock, mHal2Mock);
+    }
+
+    @Test
+    public void loadModules_forHidlImpl() {
+        assertWithMessage("Modules loaded in HIDL HAL")
+                .that(mHidlImpl.listModules())
+                .containsExactly(mHal1ModuleMock, mHal2ModuleMock);
+    }
+
+    @Test
+    public void openTuner_withHal1ModuleId_forHidlImpl() throws Exception {
+        ITuner tuner = mHidlImpl.openTuner(HAL1_MODULE_ID, mBandConfigMock,
+                /* withAudio= */ true, mTunerCallbackMock);
+
+        assertWithMessage("Tuner opened in HAL 1")
+                .that(tuner).isEqualTo(mHal1TunerMock);
+    }
+
+    @Test
+    public void openTuner_withHal2ModuleId_forHidlImpl() throws Exception {
+        ITuner tuner = mHidlImpl.openTuner(HAL1_MODULE_ID + 1, mBandConfigMock,
+                /* withAudio= */ true, mTunerCallbackMock);
+
+        assertWithMessage("Tuner opened in HAL 2")
+                .that(tuner).isEqualTo(mHal2TunerMock);
+    }
+
+    @Test
+    public void addAnnouncementListener_forHidlImpl() {
+        when(mHal2Mock.hasAnyModules()).thenReturn(true);
+        ICloseHandle closeHandle = mHidlImpl.addAnnouncementListener(ENABLE_TYPES, mListenerMock);
+
+        verify(mHal2Mock).addAnnouncementListener(ENABLE_TYPES, mListenerMock);
+        assertWithMessage("Close handle of announcement listener for HAL 2")
+                .that(closeHandle).isEqualTo(mICloseHandle);
+    }
+
+}
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 0bc7085..3ee20ea 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -321,4 +321,21 @@
 
     <!-- The smaller size of the dismiss target (shrinks when something is in the target). -->
     <dimen name="floating_dismiss_circle_small">120dp</dimen>
+
+    <!-- The thickness of shadows of a window that has focus in DIP. -->
+    <dimen name="freeform_decor_shadow_focused_thickness">20dp</dimen>
+
+    <!-- The thickness of shadows of a window that doesn't have focus in DIP. -->
+    <dimen name="freeform_decor_shadow_unfocused_thickness">5dp</dimen>
+
+    <!-- Height of button (32dp)  + 2 * margin (5dp each). -->
+    <dimen name="freeform_decor_caption_height">42dp</dimen>
+
+    <!-- Width of buttons (64dp) + handle (128dp) + padding (24dp total). -->
+    <dimen name="freeform_decor_caption_width">216dp</dimen>
+
+    <dimen name="freeform_resize_handle">30dp</dimen>
+
+    <dimen name="freeform_resize_corner">44dp</dimen>
+
 </resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 87700ee..7d1f130 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -21,7 +21,6 @@
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.graphics.Color;
-import android.graphics.Rect;
 import android.graphics.drawable.VectorDrawable;
 import android.os.Handler;
 import android.view.Choreographer;
@@ -43,22 +42,6 @@
  * The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't.
  */
 public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> {
-    // The thickness of shadows of a window that has focus in DIP.
-    private static final int DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP = 20;
-    // The thickness of shadows of a window that doesn't have focus in DIP.
-    private static final int DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP = 5;
-
-    // Height of button (32dp)  + 2 * margin (5dp each)
-    private static final int DECOR_CAPTION_HEIGHT_IN_DIP = 42;
-    // Width of buttons (64dp) + handle (128dp) + padding (24dp total)
-    private static final int DECOR_CAPTION_WIDTH_IN_DIP = 216;
-    private static final int RESIZE_HANDLE_IN_DIP = 30;
-    private static final int RESIZE_CORNER_IN_DIP = 44;
-
-    private static final Rect EMPTY_OUTSET = new Rect();
-    private static final Rect RESIZE_HANDLE_OUTSET = new Rect(
-            RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP);
-
     private final Handler mHandler;
     private final Choreographer mChoreographer;
     private final SyncTransactionQueue mSyncQueue;
@@ -69,6 +52,7 @@
 
     private DragResizeInputListener mDragResizeListener;
 
+    private RelayoutParams mRelayoutParams = new RelayoutParams();
     private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult =
             new WindowDecoration.RelayoutResult<>();
 
@@ -114,19 +98,31 @@
 
     void relayout(ActivityManager.RunningTaskInfo taskInfo,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
-        final int shadowRadiusDp = taskInfo.isFocused
-                ? DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP : DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP;
+        final int shadowRadiusID = taskInfo.isFocused
+                ? R.dimen.freeform_decor_shadow_focused_thickness
+                : R.dimen.freeform_decor_shadow_unfocused_thickness;
         final boolean isFreeform = mTaskInfo.configuration.windowConfiguration.getWindowingMode()
                 == WindowConfiguration.WINDOWING_MODE_FREEFORM;
         final boolean isDragResizeable = isFreeform && mTaskInfo.isResizeable;
-        final Rect outset = isDragResizeable ? RESIZE_HANDLE_OUTSET : EMPTY_OUTSET;
 
         WindowDecorLinearLayout oldRootView = mResult.mRootView;
         final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
         final WindowContainerTransaction wct = new WindowContainerTransaction();
-        relayout(taskInfo, R.layout.caption_window_decoration, oldRootView,
-                DECOR_CAPTION_HEIGHT_IN_DIP, DECOR_CAPTION_WIDTH_IN_DIP, outset, shadowRadiusDp,
-                startT, finishT, wct, mResult);
+
+        int outsetLeftId = R.dimen.freeform_resize_handle;
+        int outsetTopId = R.dimen.freeform_resize_handle;
+        int outsetRightId = R.dimen.freeform_resize_handle;
+        int outsetBottomId = R.dimen.freeform_resize_handle;
+
+        mRelayoutParams.mRunningTaskInfo = taskInfo;
+        mRelayoutParams.mLayoutResId = R.layout.caption_window_decoration;
+        mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height;
+        mRelayoutParams.mCaptionWidthId = R.dimen.freeform_decor_caption_width;
+        mRelayoutParams.mShadowRadiusId = shadowRadiusID;
+        if (isDragResizeable) {
+            mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId);
+        }
+        relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
 
         mTaskOrganizer.applyTransaction(wct);
 
@@ -167,10 +163,12 @@
         }
 
         int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()).getScaledTouchSlop();
-
+        int resize_handle = mResult.mRootView.getResources()
+                .getDimensionPixelSize(R.dimen.freeform_resize_handle);
+        int resize_corner = mResult.mRootView.getResources()
+                .getDimensionPixelSize(R.dimen.freeform_resize_corner);
         mDragResizeListener.setGeometry(
-                mResult.mWidth, mResult.mHeight, (int) (mResult.mDensity * RESIZE_HANDLE_IN_DIP),
-                (int) (mResult.mDensity * RESIZE_CORNER_IN_DIP), touchSlop);
+                mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop);
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index bf863ea..01cab9a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -19,11 +19,11 @@
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.PixelFormat;
 import android.graphics.Point;
 import android.graphics.Rect;
-import android.util.DisplayMetrics;
 import android.view.Display;
 import android.view.InsetsState;
 import android.view.LayoutInflater;
@@ -142,15 +142,14 @@
      */
     abstract void relayout(RunningTaskInfo taskInfo);
 
-    void relayout(RunningTaskInfo taskInfo, int layoutResId, T rootView, float captionHeightDp,
-            float captionWidthDp, Rect outsetsDp, float shadowRadiusDp,
-            SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
-            WindowContainerTransaction wct, RelayoutResult<T> outResult) {
+    void relayout(RelayoutParams params, SurfaceControl.Transaction startT,
+            SurfaceControl.Transaction finishT, WindowContainerTransaction wct, T rootView,
+            RelayoutResult<T> outResult) {
         outResult.reset();
 
         final Configuration oldTaskConfig = mTaskInfo.getConfiguration();
-        if (taskInfo != null) {
-            mTaskInfo = taskInfo;
+        if (params.mRunningTaskInfo != null) {
+            mTaskInfo = params.mRunningTaskInfo;
         }
 
         if (!mTaskInfo.isVisible) {
@@ -159,7 +158,7 @@
             return;
         }
 
-        if (rootView == null && layoutResId == 0) {
+        if (rootView == null && params.mLayoutResId == 0) {
             throw new IllegalArgumentException("layoutResId and rootView can't both be invalid.");
         }
 
@@ -176,15 +175,15 @@
                 return;
             }
             mDecorWindowContext = mContext.createConfigurationContext(taskConfig);
-            if (layoutResId != 0) {
-                outResult.mRootView =
-                        (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null);
+            if (params.mLayoutResId != 0) {
+                outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext)
+                                .inflate(params.mLayoutResId, null);
             }
         }
 
         if (outResult.mRootView == null) {
-            outResult.mRootView =
-                    (T) LayoutInflater.from(mDecorWindowContext).inflate(layoutResId, null);
+            outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext)
+                            .inflate(params.mLayoutResId , null);
         }
 
         // DecorationContainerSurface
@@ -200,18 +199,18 @@
         }
 
         final Rect taskBounds = taskConfig.windowConfiguration.getBounds();
-        outResult.mDensity = taskConfig.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
-        final int decorContainerOffsetX = -(int) (outsetsDp.left * outResult.mDensity);
-        final int decorContainerOffsetY = -(int) (outsetsDp.top * outResult.mDensity);
+        final int decorContainerOffsetX = -loadResource(params.mOutsetLeftId);
+        final int decorContainerOffsetY = -loadResource(params.mOutsetTopId);
         outResult.mWidth = taskBounds.width()
-                + (int) (outsetsDp.right * outResult.mDensity)
+                + loadResource(params.mOutsetRightId)
                 - decorContainerOffsetX;
         outResult.mHeight = taskBounds.height()
-                + (int) (outsetsDp.bottom * outResult.mDensity)
+                + loadResource(params.mOutsetBottomId)
                 - decorContainerOffsetY;
         startT.setPosition(
                         mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY)
-                .setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight)
+                .setWindowCrop(mDecorationContainerSurface,
+                        outResult.mWidth, outResult.mHeight)
                 // TODO(b/244455401): Change the z-order when it's better organized
                 .setLayer(mDecorationContainerSurface, mTaskInfo.numActivities + 1)
                 .show(mDecorationContainerSurface);
@@ -226,12 +225,13 @@
                     .build();
         }
 
-        float shadowRadius = outResult.mDensity * shadowRadiusDp;
+        float shadowRadius = loadResource(params.mShadowRadiusId);
         int backgroundColorInt = mTaskInfo.taskDescription.getBackgroundColor();
         mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f;
         mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f;
         mTmpColor[2] = (float) Color.blue(backgroundColorInt) / 255.f;
-        startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(), taskBounds.height())
+        startT.setWindowCrop(mTaskBackgroundSurface, taskBounds.width(),
+                        taskBounds.height())
                 .setShadowRadius(mTaskBackgroundSurface, shadowRadius)
                 .setColor(mTaskBackgroundSurface, mTmpColor)
                 // TODO(b/244455401): Change the z-order when it's better organized
@@ -248,8 +248,8 @@
                     .build();
         }
 
-        final int captionHeight = (int) Math.ceil(captionHeightDp * outResult.mDensity);
-        final int captionWidth = (int) Math.ceil(captionWidthDp * outResult.mDensity);
+        final int captionHeight = loadResource(params.mCaptionHeightId);
+        final int captionWidth = loadResource(params.mCaptionWidthId);
 
         //Prevent caption from going offscreen if task is too high up
         final int captionYPos = taskBounds.top <= captionHeight / 2 ? 0 : captionHeight / 2;
@@ -289,8 +289,10 @@
 
             // Caption insets
             mCaptionInsetsRect.set(taskBounds);
-            mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + captionHeight - captionYPos;
-            wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect, CAPTION_INSETS_TYPES);
+            mCaptionInsetsRect.bottom =
+                    mCaptionInsetsRect.top + captionHeight - captionYPos;
+            wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect,
+                    CAPTION_INSETS_TYPES);
         } else {
             startT.hide(mCaptionContainerSurface);
         }
@@ -307,6 +309,13 @@
                 .setCrop(mTaskSurface, mTaskSurfaceCrop);
     }
 
+    private int loadResource(int resourceId) {
+        if (resourceId == Resources.ID_NULL) {
+            return 0;
+        }
+        return mDecorWindowContext.getResources().getDimensionPixelSize(resourceId);
+    }
+
     /**
      * Obtains the {@link Display} instance for the display ID in {@link #mTaskInfo} if it exists or
      * registers {@link #mOnDisplaysChangedListener} if it doesn't.
@@ -368,13 +377,11 @@
     static class RelayoutResult<T extends View & TaskFocusStateConsumer> {
         int mWidth;
         int mHeight;
-        float mDensity;
         T mRootView;
 
         void reset() {
             mWidth = 0;
             mHeight = 0;
-            mDensity = 0;
             mRootView = null;
         }
     }
@@ -395,4 +402,37 @@
             return new SurfaceControlViewHost(c, d, wmm);
         }
     }
+
+    static class RelayoutParams{
+        RunningTaskInfo mRunningTaskInfo;
+        int mLayoutResId;
+        int mCaptionHeightId;
+        int mCaptionWidthId;
+        int mShadowRadiusId;
+
+        int mOutsetTopId;
+        int mOutsetBottomId;
+        int mOutsetLeftId;
+        int mOutsetRightId;
+
+        void setOutsets(int leftId, int topId, int rightId, int bottomId) {
+            mOutsetLeftId = leftId;
+            mOutsetTopId = topId;
+            mOutsetRightId = rightId;
+            mOutsetBottomId = bottomId;
+        }
+
+        void reset() {
+            mLayoutResId = Resources.ID_NULL;
+            mCaptionHeightId = Resources.ID_NULL;
+            mCaptionWidthId = Resources.ID_NULL;
+            mShadowRadiusId = Resources.ID_NULL;
+
+            mOutsetTopId = Resources.ID_NULL;
+            mOutsetBottomId = Resources.ID_NULL;
+            mOutsetLeftId = Resources.ID_NULL;
+            mOutsetRightId = Resources.ID_NULL;
+        }
+
+    }
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index fa62b9c..103c8da 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -51,6 +51,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
@@ -76,13 +77,9 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 public class WindowDecorationTests extends ShellTestCase {
-    private static final int CAPTION_HEIGHT_DP = 32;
-    private static final int CAPTION_WIDTH_DP = 216;
-    private static final int SHADOW_RADIUS_DP = 5;
     private static final Rect TASK_BOUNDS = new Rect(100, 300, 400, 400);
     private static final Point TASK_POSITION_IN_PARENT = new Point(40, 60);
 
-    private final Rect mOutsetsDp = new Rect();
     private final WindowDecoration.RelayoutResult<TestView> mRelayoutResult =
             new WindowDecoration.RelayoutResult<>();
 
@@ -104,6 +101,7 @@
     private final List<SurfaceControl.Builder> mMockSurfaceControlBuilders = new ArrayList<>();
     private SurfaceControl.Transaction mMockSurfaceControlStartT;
     private SurfaceControl.Transaction mMockSurfaceControlFinishT;
+    private WindowDecoration.RelayoutParams mRelayoutParams = new WindowDecoration.RelayoutParams();
 
     @Before
     public void setUp() {
@@ -147,7 +145,8 @@
         // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
         // 64px.
         taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
-        mOutsetsDp.set(10, 20, 30, 40);
+        mRelayoutParams.setOutsets(R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle,
+                R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle);
 
         final SurfaceControl taskSurface = mock(SurfaceControl.class);
         final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
@@ -197,8 +196,13 @@
         // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
         // 64px.
         taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
-        mOutsetsDp.set(10, 20, 30, 40);
-
+//        int outsetLeftId = R.dimen.split_divider_bar_width;
+//        int outsetTopId = R.dimen.gestures_onehanded_drag_threshold;
+//        int outsetRightId = R.dimen.freeform_resize_handle;
+//        int outsetBottomId = R.dimen.bubble_dismiss_target_padding_x;
+//        mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId);
+        mRelayoutParams.setOutsets(R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle,
+                R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle);
         final SurfaceControl taskSurface = mock(SurfaceControl.class);
         final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
 
@@ -207,8 +211,8 @@
         verify(decorContainerSurfaceBuilder).setParent(taskSurface);
         verify(decorContainerSurfaceBuilder).setContainerLayer();
         verify(mMockSurfaceControlStartT).setTrustedOverlay(decorContainerSurface, true);
-        verify(mMockSurfaceControlStartT).setPosition(decorContainerSurface, -20, -40);
-        verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 380, 220);
+        verify(mMockSurfaceControlStartT).setPosition(decorContainerSurface, -60, -60);
+        verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 420, 220);
 
         verify(taskBackgroundSurfaceBuilder).setParent(taskSurface);
         verify(taskBackgroundSurfaceBuilder).setEffectLayer();
@@ -221,34 +225,36 @@
 
         verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface);
         verify(captionContainerSurfaceBuilder).setContainerLayer();
-        verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, -46, 8);
-        verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64);
+        verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, -6, -156);
+        verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 432);
         verify(mMockSurfaceControlStartT).show(captionContainerSurface);
 
         verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any());
+
         verify(mMockSurfaceControlViewHost)
                 .setView(same(mMockView),
-                        argThat(lp -> lp.height == 64
-                                && lp.width == 300
+                        argThat(lp -> lp.height == 432
+                                && lp.width == 432
                                 && (lp.flags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0));
         if (ViewRootImpl.CAPTION_ON_SHELL) {
             verify(mMockView).setTaskFocusState(true);
             verify(mMockWindowContainerTransaction)
                     .addRectInsetsProvider(taskInfo.token,
-                            new Rect(100, 300, 400, 364),
+                            new Rect(100, 300, 400, 516),
                             new int[] { InsetsState.ITYPE_CAPTION_BAR });
         }
 
         verify(mMockSurfaceControlFinishT)
                 .setPosition(taskSurface, TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y);
         verify(mMockSurfaceControlFinishT)
-                .setCrop(taskSurface, new Rect(-20, -40, 360, 180));
+                .setCrop(taskSurface, new Rect(-60, -60, 360, 160));
         verify(mMockSurfaceControlStartT)
                 .show(taskSurface);
 
-        assertEquals(380, mRelayoutResult.mWidth);
+        assertEquals(420, mRelayoutResult.mWidth);
         assertEquals(220, mRelayoutResult.mHeight);
-        assertEquals(2, mRelayoutResult.mDensity, 0.f);
+
+
     }
 
     @Test
@@ -287,7 +293,8 @@
         // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
         // 64px.
         taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
-        mOutsetsDp.set(10, 20, 30, 40);
+        mRelayoutParams.setOutsets(R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle,
+                R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle);
 
         final SurfaceControl taskSurface = mock(SurfaceControl.class);
         final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
@@ -410,9 +417,15 @@
 
         @Override
         void relayout(ActivityManager.RunningTaskInfo taskInfo) {
-            relayout(null /* taskInfo */, 0 /* layoutResId */, mMockView, CAPTION_HEIGHT_DP,
-                    CAPTION_WIDTH_DP, mOutsetsDp, SHADOW_RADIUS_DP, mMockSurfaceControlStartT,
-                    mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mRelayoutResult);
+
+            mRelayoutParams.mLayoutResId = 0;
+            mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_width;
+            mRelayoutParams.mCaptionWidthId = R.dimen.freeform_decor_caption_width;
+            mRelayoutParams.mShadowRadiusId =
+                    R.dimen.freeform_decor_shadow_unfocused_thickness;
+
+            relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT,
+                    mMockWindowContainerTransaction, mMockView, mRelayoutResult);
         }
     }
 }
diff --git a/media/java/android/media/MediaCrypto.java b/media/java/android/media/MediaCrypto.java
index 889a5f7..1930262 100644
--- a/media/java/android/media/MediaCrypto.java
+++ b/media/java/android/media/MediaCrypto.java
@@ -75,14 +75,17 @@
     public final native boolean requiresSecureDecoderComponent(@NonNull String mime);
 
     /**
-     * Associate a MediaDrm session with this MediaCrypto instance.  The
-     * MediaDrm session is used to securely load decryption keys for a
-     * crypto scheme.  The crypto keys loaded through the MediaDrm session
+     * Associate a new MediaDrm session with this MediaCrypto instance.
+     *
+     * <p>The MediaDrm session is used to securely load decryption keys for a
+     * crypto scheme. The crypto keys loaded through the MediaDrm session
      * may be selected for use during the decryption operation performed
      * by {@link android.media.MediaCodec#queueSecureInputBuffer} by specifying
-     * their key ids in the {@link android.media.MediaCodec.CryptoInfo#key} field.
-     * @param sessionId the MediaDrm sessionId to associate with this
-     * MediaCrypto instance
+     * their key IDs in the {@link android.media.MediaCodec.CryptoInfo#key} field.
+     *
+     * @param sessionId The MediaDrm sessionId to associate with this MediaCrypto
+     *         instance. The session's scheme must match the scheme UUID used when
+     *         constructing this MediaCrypto instance.
      * @throws MediaCryptoException on failure to set the sessionId
      */
     public final native void setMediaDrmSession(@NonNull byte[] sessionId)
diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java
index 5781537..681e112 100644
--- a/media/java/android/media/MediaRoute2Info.java
+++ b/media/java/android/media/MediaRoute2Info.java
@@ -539,9 +539,9 @@
     }
 
     /**
-     * Gets the Deduplication ID of the route if available.
-     * @see RouteDiscoveryPreference#shouldRemoveDuplicates()
-     * @hide
+     * Gets the deduplication IDs associated to the route.
+     *
+     * <p>Two routes with a matching deduplication ID originate from the same receiver device.
      */
     @NonNull
     public Set<String> getDeduplicationIds() {
@@ -1017,13 +1017,7 @@
         }
 
         /**
-         * Sets the deduplication ID of the route.
-         * Routes have the same ID could be removed even when
-         * they are from different providers.
-         * <p>
-         * If it's {@code null}, the route will not be removed.
-         * @see RouteDiscoveryPreference#shouldRemoveDuplicates()
-         * @hide
+         * Sets the {@link MediaRoute2Info#getDeduplicationIds() deduplication IDs} of the route.
          */
         @NonNull
         public Builder setDeduplicationIds(@NonNull Set<String> id) {
diff --git a/packages/CarrierDefaultApp/Android.bp b/packages/CarrierDefaultApp/Android.bp
index fc753da..1e56a93 100644
--- a/packages/CarrierDefaultApp/Android.bp
+++ b/packages/CarrierDefaultApp/Android.bp
@@ -10,6 +10,7 @@
 android_app {
     name: "CarrierDefaultApp",
     srcs: ["src/**/*.java"],
+    static_libs: ["SliceStore"],
     platform_apis: true,
     certificate: "platform",
 }
diff --git a/packages/CarrierDefaultApp/AndroidManifest.xml b/packages/CarrierDefaultApp/AndroidManifest.xml
index 632dfb3..9566f22 100644
--- a/packages/CarrierDefaultApp/AndroidManifest.xml
+++ b/packages/CarrierDefaultApp/AndroidManifest.xml
@@ -71,5 +71,22 @@
                 <data android:host="*" />
             </intent-filter>
         </activity-alias>
+
+        <receiver android:name="com.android.carrierdefaultapp.SliceStoreBroadcastReceiver"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.phone.slicestore.action.START_SLICE_STORE" />
+                <action android:name="com.android.phone.slicestore.action.SLICE_STORE_RESPONSE_TIMEOUT" />
+                <action android:name="com.android.phone.slicestore.action.NOTIFICATION_CANCELED" />
+            </intent-filter>
+        </receiver>
+        <activity android:name="com.android.carrierdefaultapp.SliceStoreActivity"
+                  android:label="@string/slice_store_label"
+                  android:exported="true"
+                  android:configChanges="keyboardHidden|orientation|screenSize">
+            <intent-filter>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
     </application>
 </manifest>
diff --git a/packages/CarrierDefaultApp/res/drawable/ic_network_boost.xml b/packages/CarrierDefaultApp/res/drawable/ic_network_boost.xml
new file mode 100644
index 0000000..ad8a21c
--- /dev/null
+++ b/packages/CarrierDefaultApp/res/drawable/ic_network_boost.xml
@@ -0,0 +1,23 @@
+<!--
+  ~ 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path android:fillColor="@android:color/white"
+          android:pathData="M3,17V15H8Q8,15 8,15Q8,15 8,15V13Q8,13 8,13Q8,13 8,13H3V7H10V9H5V11H8Q8.825,11 9.413,11.587Q10,12.175 10,13V15Q10,15.825 9.413,16.413Q8.825,17 8,17ZM21,11V15Q21,15.825 20.413,16.413Q19.825,17 19,17H14Q13.175,17 12.588,16.413Q12,15.825 12,15V9Q12,8.175 12.588,7.587Q13.175,7 14,7H19Q19.825,7 20.413,7.587Q21,8.175 21,9H14Q14,9 14,9Q14,9 14,9V15Q14,15 14,15Q14,15 14,15H19Q19,15 19,15Q19,15 19,15V13H16.5V11Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/CarrierDefaultApp/res/values/strings.xml b/packages/CarrierDefaultApp/res/values/strings.xml
index 65a7cec..ce88a40 100644
--- a/packages/CarrierDefaultApp/res/values/strings.xml
+++ b/packages/CarrierDefaultApp/res/values/strings.xml
@@ -13,4 +13,18 @@
     <string name="ssl_error_warning">The network you&#8217;re trying to join has security issues.</string>
     <string name="ssl_error_example">For example, the login page may not belong to the organization shown.</string>
     <string name="ssl_error_continue">Continue anyway via browser</string>
+
+    <!-- Telephony notification channel name for network boost notifications. -->
+    <string name="network_boost_notification_channel">Network Boost</string>
+    <!-- Notification title text for the network boost notification. -->
+    <string name="network_boost_notification_title">%s recommends a data boost</string>
+    <!-- Notification detail text for the network boost notification. -->
+    <string name="network_boost_notification_detail">Buy a network boost for better performance</string>
+    <!-- Notification button text to cancel the network boost notification. -->
+    <string name="network_boost_notification_button_not_now">Not now</string>
+    <!-- Notification button text to manage the network boost notification. -->
+    <string name="network_boost_notification_button_manage">Manage</string>
+
+    <!-- Label to display when the slice store opens. -->
+    <string name="slice_store_label">Purchase a network boost.</string>
 </resources>
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java
new file mode 100644
index 0000000..24cb5f9
--- /dev/null
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java
@@ -0,0 +1,122 @@
+/*
+ * 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.carrierdefaultapp;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.NotificationManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.webkit.WebView;
+
+import com.android.phone.slicestore.SliceStore;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Activity that launches when the user clicks on the network boost notification.
+ */
+public class SliceStoreActivity extends Activity {
+    private static final String TAG = "SliceStoreActivity";
+
+    private URL mUrl;
+    private WebView mWebView;
+    private int mPhoneId;
+    private int mSubId;
+    private @TelephonyManager.PremiumCapability int mCapability;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Intent intent = getIntent();
+        mPhoneId = intent.getIntExtra(SliceStore.EXTRA_PHONE_ID,
+                SubscriptionManager.INVALID_PHONE_INDEX);
+        mSubId = intent.getIntExtra(SliceStore.EXTRA_SUB_ID,
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+        mCapability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY,
+                SliceStore.PREMIUM_CAPABILITY_INVALID);
+        mUrl = getUrl();
+        logd("onCreate: mPhoneId=" + mPhoneId + ", mSubId=" + mSubId + ", mCapability="
+                + TelephonyManager.convertPremiumCapabilityToString(mCapability)
+                + ", mUrl=" + mUrl);
+        getApplicationContext().getSystemService(NotificationManager.class)
+                .cancel(SliceStoreBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG, mCapability);
+        if (!SliceStoreBroadcastReceiver.isIntentValid(intent)) {
+            loge("Not starting SliceStoreActivity with an invalid Intent: " + intent);
+            SliceStoreBroadcastReceiver.sendSliceStoreResponse(
+                    intent, SliceStore.EXTRA_INTENT_REQUEST_FAILED);
+            finishAndRemoveTask();
+            return;
+        }
+        if (mUrl == null) {
+            loge("Unable to create a URL from carrier configs.");
+            SliceStoreBroadcastReceiver.sendSliceStoreResponse(
+                    intent, SliceStore.EXTRA_INTENT_CARRIER_ERROR);
+            finishAndRemoveTask();
+            return;
+        }
+        if (mSubId != SubscriptionManager.getDefaultSubscriptionId()) {
+            loge("Unable to start SliceStore on the non-default data subscription: " + mSubId);
+            SliceStoreBroadcastReceiver.sendSliceStoreResponse(
+                    intent, SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA);
+            finishAndRemoveTask();
+            return;
+        }
+
+        SliceStoreBroadcastReceiver.updateSliceStoreActivity(mCapability, this);
+
+        mWebView = new WebView(getApplicationContext());
+        setContentView(mWebView);
+        mWebView.loadUrl(mUrl.toString());
+        // TODO(b/245882601): Get back response from WebView
+    }
+
+    @Override
+    protected void onDestroy() {
+        logd("onDestroy: User canceled the purchase by closing the application.");
+        SliceStoreBroadcastReceiver.sendSliceStoreResponse(
+                getIntent(), SliceStore.EXTRA_INTENT_CANCELED);
+        SliceStoreBroadcastReceiver.removeSliceStoreActivity(mCapability);
+        super.onDestroy();
+    }
+
+    private @Nullable URL getUrl() {
+        String url = getApplicationContext().getSystemService(CarrierConfigManager.class)
+                .getConfigForSubId(mSubId).getString(
+                        CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING);
+        try {
+            return new URL(url);
+        } catch (MalformedURLException e) {
+            loge("Invalid URL: " + url);
+        }
+        return null;
+    }
+
+    private static void logd(@NonNull String s) {
+        Log.d(TAG, s);
+    }
+
+    private static void loge(@NonNull String s) {
+        Log.e(TAG, s);
+    }
+}
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java
new file mode 100644
index 0000000..7eb851d
--- /dev/null
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java
@@ -0,0 +1,315 @@
+/*
+ * 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.carrierdefaultapp;
+
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.os.UserHandle;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.WebView;
+
+import com.android.phone.slicestore.SliceStore;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The SliceStoreBroadcastReceiver listens for {@link SliceStore#ACTION_START_SLICE_STORE} from the
+ * SliceStore in the phone process to start the SliceStore application. It displays the network
+ * boost notification to the user and will start the {@link SliceStoreActivity} to display the
+ * {@link WebView} to purchase network boosts from the user's carrier.
+ */
+public class SliceStoreBroadcastReceiver extends BroadcastReceiver{
+    private static final String TAG = "SliceStoreBroadcastReceiver";
+
+    /** Weak references to {@link SliceStoreActivity} for each capability, if it exists. */
+    private static final Map<Integer, WeakReference<SliceStoreActivity>> sSliceStoreActivities =
+            new HashMap<>();
+
+    /** Channel ID for the network boost notification. */
+    private static final String NETWORK_BOOST_NOTIFICATION_CHANNEL_ID = "network_boost";
+    /** Tag for the network boost notification. */
+    public static final String NETWORK_BOOST_NOTIFICATION_TAG = "SliceStore.Notification";
+    /** Action for when the user clicks the "Not now" button on the network boost notification. */
+    private static final String ACTION_NOTIFICATION_CANCELED =
+            "com.android.phone.slicestore.action.NOTIFICATION_CANCELED";
+
+    /**
+     * Create a weak reference to {@link SliceStoreActivity}. The reference will be removed when
+     * {@link SliceStoreActivity#onDestroy()} is called.
+     *
+     * @param capability The premium capability requested.
+     * @param sliceStoreActivity The instance of SliceStoreActivity.
+     */
+    public static void updateSliceStoreActivity(@TelephonyManager.PremiumCapability int capability,
+            @NonNull SliceStoreActivity sliceStoreActivity) {
+        sSliceStoreActivities.put(capability, new WeakReference<>(sliceStoreActivity));
+    }
+
+    /**
+     * Remove the weak reference to {@link SliceStoreActivity} when
+     * {@link SliceStoreActivity#onDestroy()} is called.
+     *
+     * @param capability The premium capability requested.
+     */
+    public static void removeSliceStoreActivity(
+            @TelephonyManager.PremiumCapability int capability) {
+        sSliceStoreActivities.remove(capability);
+    }
+
+    /**
+     * Send the PendingIntent containing the corresponding SliceStore response.
+     *
+     * @param intent The Intent containing the PendingIntent extra.
+     * @param extra The extra to get the PendingIntent to send.
+     */
+    public static void sendSliceStoreResponse(@NonNull Intent intent, @NonNull String extra) {
+        PendingIntent pendingIntent = intent.getParcelableExtra(extra, PendingIntent.class);
+        if (pendingIntent == null) {
+            loge("PendingIntent does not exist for extra: " + extra);
+            return;
+        }
+        try {
+            pendingIntent.send();
+        } catch (PendingIntent.CanceledException e) {
+            loge("Unable to send " + getPendingIntentType(extra) + " intent: " + e);
+        }
+    }
+
+    /**
+     * Check whether the Intent is valid and can be used to complete purchases in the SliceStore.
+     * This checks that all necessary extras exist and that the values are valid.
+     *
+     * @param intent The intent to check
+     * @return {@code true} if the intent is valid and {@code false} otherwise.
+     */
+    public static boolean isIntentValid(@NonNull Intent intent) {
+        int phoneId = intent.getIntExtra(SliceStore.EXTRA_PHONE_ID,
+                SubscriptionManager.INVALID_PHONE_INDEX);
+        if (phoneId == SubscriptionManager.INVALID_PHONE_INDEX) {
+            loge("isIntentValid: invalid phone index: " + phoneId);
+            return false;
+        }
+
+        int subId = intent.getIntExtra(SliceStore.EXTRA_SUB_ID,
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+            loge("isIntentValid: invalid subscription ID: " + subId);
+            return false;
+        }
+
+        int capability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY,
+                SliceStore.PREMIUM_CAPABILITY_INVALID);
+        if (capability == SliceStore.PREMIUM_CAPABILITY_INVALID) {
+            loge("isIntentValid: invalid premium capability: " + capability);
+            return false;
+        }
+
+        String appName = intent.getStringExtra(SliceStore.EXTRA_REQUESTING_APP_NAME);
+        if (TextUtils.isEmpty(appName)) {
+            loge("isIntentValid: empty requesting application name: " + appName);
+            return false;
+        }
+
+        return isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_CANCELED)
+                && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_CARRIER_ERROR)
+                && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_REQUEST_FAILED)
+                && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA);
+    }
+
+    private static boolean isPendingIntentValid(@NonNull Intent intent, @NonNull String extra) {
+        String intentType = getPendingIntentType(extra);
+        PendingIntent pendingIntent = intent.getParcelableExtra(extra, PendingIntent.class);
+        if (pendingIntent == null) {
+            loge("isPendingIntentValid: " + intentType + " intent not found.");
+            return false;
+        } else if (pendingIntent.getCreatorPackage().equals(TelephonyManager.PHONE_PROCESS_NAME)) {
+            return true;
+        }
+        loge("isPendingIntentValid: " + intentType + " intent was created by "
+                + pendingIntent.getCreatorPackage() + " instead of the phone process.");
+        return false;
+    }
+
+    @NonNull private static String getPendingIntentType(@NonNull String extra) {
+        switch (extra) {
+            case SliceStore.EXTRA_INTENT_CANCELED: return "canceled";
+            case SliceStore.EXTRA_INTENT_CARRIER_ERROR: return "carrier error";
+            case SliceStore.EXTRA_INTENT_REQUEST_FAILED: return "request failed";
+            case SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA: return "not default data";
+            default: {
+                loge("Unknown pending intent extra: " + extra);
+                return "unknown(" + extra + ")";
+            }
+        }
+    }
+
+    @Override
+    public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+        logd("onReceive intent: " + intent.getAction());
+        switch (intent.getAction()) {
+            case SliceStore.ACTION_START_SLICE_STORE:
+                onDisplayBoosterNotification(context, intent);
+                break;
+            case SliceStore.ACTION_SLICE_STORE_RESPONSE_TIMEOUT:
+                onTimeout(context, intent);
+                break;
+            case ACTION_NOTIFICATION_CANCELED:
+                onUserCanceled(context, intent);
+                break;
+            default:
+                loge("Received unknown action: " + intent.getAction());
+        }
+    }
+
+    private void onDisplayBoosterNotification(@NonNull Context context, @NonNull Intent intent) {
+        if (!isIntentValid(intent)) {
+            sendSliceStoreResponse(intent, SliceStore.EXTRA_INTENT_REQUEST_FAILED);
+            return;
+        }
+
+        context.getSystemService(NotificationManager.class).createNotificationChannel(
+                new NotificationChannel(NETWORK_BOOST_NOTIFICATION_CHANNEL_ID,
+                        context.getResources().getString(
+                                R.string.network_boost_notification_channel),
+                        NotificationManager.IMPORTANCE_DEFAULT));
+
+        Notification notification =
+                new Notification.Builder(context, NETWORK_BOOST_NOTIFICATION_CHANNEL_ID)
+                        .setContentTitle(String.format(context.getResources().getString(
+                                R.string.network_boost_notification_title),
+                                intent.getStringExtra(SliceStore.EXTRA_REQUESTING_APP_NAME)))
+                        .setContentText(context.getResources().getString(
+                                R.string.network_boost_notification_detail))
+                        .setSmallIcon(R.drawable.ic_network_boost)
+                        .setContentIntent(createContentIntent(context, intent, 1))
+                        .setDeleteIntent(intent.getParcelableExtra(
+                                SliceStore.EXTRA_INTENT_CANCELED, PendingIntent.class))
+                        // Add an action for the "Not now" button, which has the same behavior as
+                        // the user canceling or closing the notification.
+                        .addAction(new Notification.Action.Builder(
+                                Icon.createWithResource(context, R.drawable.ic_network_boost),
+                                context.getResources().getString(
+                                        R.string.network_boost_notification_button_not_now),
+                                createCanceledIntent(context, intent)).build())
+                        // Add an action for the "Manage" button, which has the same behavior as
+                        // the user clicking on the notification.
+                        .addAction(new Notification.Action.Builder(
+                                Icon.createWithResource(context, R.drawable.ic_network_boost),
+                                context.getResources().getString(
+                                        R.string.network_boost_notification_button_manage),
+                                createContentIntent(context, intent, 2)).build())
+                        .build();
+
+        int capability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY,
+                SliceStore.PREMIUM_CAPABILITY_INVALID);
+        logd("Display the booster notification for capability "
+                + TelephonyManager.convertPremiumCapabilityToString(capability));
+        context.getSystemService(NotificationManager.class).notifyAsUser(
+                NETWORK_BOOST_NOTIFICATION_TAG, capability, notification, UserHandle.ALL);
+    }
+
+    /**
+     * Create the intent for when the user clicks on the "Manage" button on the network boost
+     * notification or the notification itself. This will open {@link SliceStoreActivity}.
+     *
+     * @param context The Context to create the intent for.
+     * @param intent The source Intent used to launch the SliceStore application.
+     * @param requestCode The request code for the PendingIntent.
+     *
+     * @return The intent to start {@link SliceStoreActivity}.
+     */
+    @NonNull private PendingIntent createContentIntent(@NonNull Context context,
+            @NonNull Intent intent, int requestCode) {
+        Intent i = new Intent(context, SliceStoreActivity.class);
+        i.setComponent(ComponentName.unflattenFromString(
+                "com.android.carrierdefaultapp/.SliceStoreActivity"));
+        i.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
+                | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        i.putExtras(intent);
+        return PendingIntent.getActivityAsUser(context, requestCode, i,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, null /* options */,
+                UserHandle.CURRENT);
+    }
+
+    /**
+     * Create the canceled intent for when the user clicks the "Not now" button on the network boost
+     * notification. This will send {@link #ACTION_NOTIFICATION_CANCELED} and has the same function
+     * as if the user had canceled or removed the notification.
+     *
+     * @param context The Context to create the intent for.
+     * @param intent The source Intent used to launch the SliceStore application.
+     *
+     * @return The canceled intent.
+     */
+    @NonNull private PendingIntent createCanceledIntent(@NonNull Context context,
+            @NonNull Intent intent) {
+        Intent i = new Intent(ACTION_NOTIFICATION_CANCELED);
+        i.setComponent(ComponentName.unflattenFromString(
+                "com.android.carrierdefaultapp/.SliceStoreBroadcastReceiver"));
+        i.putExtras(intent);
+        return PendingIntent.getBroadcast(context, 0, i,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
+    }
+
+    private void onTimeout(@NonNull Context context, @NonNull Intent intent) {
+        int capability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY,
+                SliceStore.PREMIUM_CAPABILITY_INVALID);
+        logd("Purchase capability " + TelephonyManager.convertPremiumCapabilityToString(capability)
+                + " timed out.");
+        if (sSliceStoreActivities.get(capability) == null) {
+            // Notification is still active
+            logd("Closing booster notification since the user did not respond in time.");
+            context.getSystemService(NotificationManager.class).cancelAsUser(
+                    NETWORK_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
+        } else {
+            // Notification was dismissed but SliceStoreActivity is still active
+            logd("Closing SliceStore WebView since the user did not complete the purchase "
+                    + "in time.");
+            sSliceStoreActivities.get(capability).get().finishAndRemoveTask();
+            // TODO: Display a toast to indicate timeout for better UX?
+        }
+    }
+
+    private void onUserCanceled(@NonNull Context context, @NonNull Intent intent) {
+        int capability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY,
+                SliceStore.PREMIUM_CAPABILITY_INVALID);
+        logd("onUserCanceled: " + TelephonyManager.convertPremiumCapabilityToString(capability));
+        context.getSystemService(NotificationManager.class)
+                .cancelAsUser(NETWORK_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
+        sendSliceStoreResponse(intent, SliceStore.EXTRA_INTENT_CANCELED);
+    }
+
+    private static void logd(String s) {
+        Log.d(TAG, s);
+    }
+
+    private static void loge(String s) {
+        Log.e(TAG, s);
+    }
+}
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index 2c24bf1..92ce772 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -1,4 +1,5 @@
-<resources>
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
   <string name="app_name">CredentialManager</string>
   <string name="string_cancel">Cancel</string>
   <string name="string_continue">Continue</string>
@@ -12,4 +13,7 @@
   <string name="choose_create_option_title">Create a passkey at</string>
   <string name="choose_sign_in_title">Use saved sign in</string>
   <string name="create_passkey_at">Create passkey at</string>
+  <string name="use_provider_for_all_title">Use <xliff:g id="providerInfoName">%1$s</xliff:g> for all your sign-ins?</string>
+  <string name="set_as_default">Set as default</string>
+  <string name="use_once">Use once</string>
 </resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 489cc27..ec0c5b7 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -21,6 +21,7 @@
 import android.app.slice.SliceSpec
 import android.content.Context
 import android.content.Intent
+import android.credentials.ui.Constants
 import android.credentials.ui.Entry
 import android.credentials.ui.ProviderData
 import android.credentials.ui.RequestInfo
@@ -60,7 +61,7 @@
     ) ?: testProviderList()
 
     resultReceiver = intent.getParcelableExtra(
-      RequestInfo.EXTRA_RESULT_RECEIVER,
+      Constants.EXTRA_RESULT_RECEIVER,
       ResultReceiver::class.java
     )
   }
@@ -118,46 +119,42 @@
   // TODO: below are prototype functionalities. To be removed for productionization.
   private fun testProviderList(): List<ProviderData> {
     return listOf(
-      ProviderData(
+      ProviderData.Builder(
         "com.google",
-        listOf<Entry>(
-          newEntry(1, "elisa.beckett@gmail.com", "Elisa Backett",
-            "20 passwords and 7 passkeys saved"),
-          newEntry(2, "elisa.work@google.com", "Elisa Backett Work",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        listOf<Entry>(
-          newEntry(3, "Go to Settings", "",
-            "20 passwords and 7 passkeys saved"),
-          newEntry(4, "Switch Account", "",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        null
-      ),
-      ProviderData(
+        "Google Password Manager",
+        Icon.createWithResource(context, R.drawable.ic_launcher_foreground))
+        .setCredentialEntries(
+          listOf<Entry>(
+            newEntry(1, "elisa.beckett@gmail.com", "Elisa Backett",
+                     "20 passwords and 7 passkeys saved"),
+            newEntry(2, "elisa.work@google.com", "Elisa Backett Work",
+                     "20 passwords and 7 passkeys saved"),
+          )
+        ).setActionChips(
+          listOf<Entry>(
+            newEntry(3, "Go to Settings", "",
+                     "20 passwords and 7 passkeys saved"),
+            newEntry(4, "Switch Account", "",
+                     "20 passwords and 7 passkeys saved"),
+          ),
+        ).build(),
+      ProviderData.Builder(
         "com.dashlane",
-        listOf<Entry>(
-          newEntry(5, "elisa.beckett@dashlane.com", "Elisa Backett",
-            "20 passwords and 7 passkeys saved"),
-          newEntry(6, "elisa.work@dashlane.com", "Elisa Backett Work",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        listOf<Entry>(
-          newEntry(7, "Manage Accounts", "Manage your accounts in the dashlane app",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        null
-      ),
-      ProviderData(
-        "com.lastpass",
-        listOf<Entry>(
-          newEntry(8, "elisa.beckett@lastpass.com", "Elisa Backett",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        listOf<Entry>(),
-        null
-      )
-
+        "Dashlane",
+        Icon.createWithResource(context, R.drawable.ic_launcher_foreground))
+        .setCredentialEntries(
+          listOf<Entry>(
+            newEntry(1, "elisa.beckett@dashlane.com", "Elisa Backett",
+                     "20 passwords and 7 passkeys saved"),
+            newEntry(2, "elisa.work@dashlane.com", "Elisa Backett Work",
+                     "20 passwords and 7 passkeys saved"),
+          )
+        ).setActionChips(
+          listOf<Entry>(
+            newEntry(3, "Manage Accounts", "Manage your accounts in the dashlane app",
+                     "20 passwords and 7 passkeys saved"),
+          ),
+        ).build(),
     )
   }
 
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
index f4d60b5..82fce9f 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
@@ -83,6 +83,8 @@
             onOptionSelected = {viewModel.onMoreOptionsRowSelected(it)}
           )
         CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard(
+          providerInfo = uiState.selectedProvider!!,
+          onDefaultOrNotSelected = {viewModel.onDefaultOrNotSelected(it)}
         )
       }
     },
@@ -282,10 +284,37 @@
 @ExperimentalMaterialApi
 @Composable
 fun MoreOptionsRowIntroCard(
+  providerInfo: ProviderInfo,
+  onDefaultOrNotSelected: (String) -> Unit,
 ) {
   Card(
     backgroundColor = lightBackgroundColor,
   ) {
+    Column() {
+      Text(
+        text = stringResource(R.string.use_provider_for_all_title, providerInfo.name),
+        style = Typography.subtitle1,
+        modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally)
+      )
+      Row(
+        horizontalArrangement = Arrangement.SpaceBetween,
+        modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
+      ) {
+        CancelButton(
+          stringResource(R.string.use_once),
+          onclick = { onDefaultOrNotSelected(providerInfo.name) }
+        )
+        ConfirmButton(
+          stringResource(R.string.set_as_default),
+          onclick = { onDefaultOrNotSelected(providerInfo.name) }
+        )
+      }
+      Divider(
+        thickness = 18.dp,
+        color = Color.Transparent,
+        modifier = Modifier.padding(bottom = 40.dp)
+      )
+    }
   }
 }
 
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
index 3cf81da..ff44e2e 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
@@ -112,4 +112,12 @@
     CredentialManagerRepo.getInstance().onCancel()
     dialogResult.value = DialogResult(ResultState.CANCELED)
   }
+
+  fun onDefaultOrNotSelected(providerName: String) {
+    uiState = uiState.copy(
+      currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
+      selectedProvider = getProviderInfoByName(providerName)
+    )
+    // TODO: implement the if choose as default or not logic later
+  }
 }
diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle
index 811cdd8..68c63da 100644
--- a/packages/SettingsLib/Spa/build.gradle
+++ b/packages/SettingsLib/Spa/build.gradle
@@ -17,6 +17,7 @@
 buildscript {
     ext {
         spa_min_sdk = 21
+        spa_target_sdk = 33
         jetpack_compose_version = '1.2.0-alpha04'
         jetpack_compose_compiler_version = '1.3.2'
         jetpack_compose_material3_version = '1.0.0-alpha06'
diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
index 0a4972f..f1a24af 100644
--- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
@@ -17,6 +17,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.settingslib.spa.gallery">
 
+    <uses-sdk android:minSdkVersion="21"/>
+
     <application
         android:name=".GalleryApplication"
         android:icon="@mipmap/ic_launcher"
@@ -32,11 +34,6 @@
             </intent-filter>
         </activity>
 
-        <activity
-            android:name=".GalleryDebugActivity"
-            android:exported="true">
-        </activity>
-
         <provider
             android:name=".GalleryEntryProvider"
             android:authorities="com.android.spa.gallery.provider"
@@ -44,5 +41,20 @@
             android:exported="false">
         </provider>
 
+        <activity
+            android:name="com.android.settingslib.spa.framework.debug.BlankActivity"
+            android:exported="true">
+        </activity>
+        <activity
+            android:name="com.android.settingslib.spa.framework.debug.DebugActivity"
+            android:exported="true">
+        </activity>
+        <provider
+            android:name="com.android.settingslib.spa.framework.debug.DebugProvider"
+            android:authorities="com.android.spa.gallery.debug"
+            android:enabled="true"
+            android:exported="false">
+        </provider>
+
     </application>
 </manifest>
diff --git a/packages/SettingsLib/Spa/gallery/build.gradle b/packages/SettingsLib/Spa/gallery/build.gradle
index 551a0b1..c1ce7d9 100644
--- a/packages/SettingsLib/Spa/gallery/build.gradle
+++ b/packages/SettingsLib/Spa/gallery/build.gradle
@@ -21,12 +21,12 @@
 
 android {
     namespace 'com.android.settingslib.spa.gallery'
-    compileSdk 33
+    compileSdk spa_target_sdk
 
     defaultConfig {
         applicationId "com.android.settingslib.spa.gallery"
         minSdk spa_min_sdk
-        targetSdk 33
+        targetSdk spa_target_sdk
         versionCode 1
         versionName "1.0"
     }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
deleted file mode 100644
index 23072a2..0000000
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDebugActivity.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.gallery
-
-import com.android.settingslib.spa.framework.DebugActivity
-
-class GalleryDebugActivity : DebugActivity()
diff --git a/packages/SettingsLib/Spa/spa/AndroidManifest.xml b/packages/SettingsLib/Spa/spa/AndroidManifest.xml
index 410bcdb..62800bd 100644
--- a/packages/SettingsLib/Spa/spa/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/spa/AndroidManifest.xml
@@ -14,4 +14,7 @@
   limitations under the License.
   -->
 
-<manifest package="com.android.settingslib.spa" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.settingslib.spa">
+    <uses-sdk android:minSdkVersion="21"/>
+</manifest>
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle
index 7e05e75..c587411 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle
+++ b/packages/SettingsLib/Spa/spa/build.gradle
@@ -21,11 +21,11 @@
 
 android {
     namespace 'com.android.settingslib.spa'
-    compileSdk 33
+    compileSdk spa_target_sdk
 
     defaultConfig {
         minSdk spa_min_sdk
-        targetSdk 33
+        targetSdk spa_target_sdk
     }
 
     sourceSets {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt
index 532f63b..d631708 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt
@@ -16,21 +16,22 @@
 
 package com.android.settingslib.spa.framework
 
-import android.content.ComponentName
 import android.content.ContentProvider
 import android.content.ContentValues
 import android.content.Context
 import android.content.Intent
-import android.content.Intent.URI_INTENT_SCHEME
 import android.content.UriMatcher
 import android.content.pm.ProviderInfo
 import android.database.Cursor
 import android.database.MatrixCursor
 import android.net.Uri
 import android.util.Log
+import com.android.settingslib.spa.framework.common.ColumnEnum
+import com.android.settingslib.spa.framework.common.QueryEnum
 import com.android.settingslib.spa.framework.common.SettingsEntry
-import com.android.settingslib.spa.framework.common.SettingsPage
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.common.addUri
+import com.android.settingslib.spa.framework.common.getColumns
 
 private const val TAG = "EntryProvider"
 
@@ -39,117 +40,15 @@
  * One can query the provider result by:
  *   $ adb shell content query --uri content://<AuthorityPath>/<QueryPath>
  * For gallery, AuthorityPath = com.android.spa.gallery.provider
- * For SettingsGoogle, AuthorityPath = com.android.settings.spa.provider
+ * For Settings, AuthorityPath = com.android.settings.spa.provider
  * Some examples:
- *   $ adb shell content query --uri content://<AuthorityPath>/page_debug
- *   $ adb shell content query --uri content://<AuthorityPath>/entry_debug
- *   $ adb shell content query --uri content://<AuthorityPath>/page_info
- *   $ adb shell content query --uri content://<AuthorityPath>/entry_info
  *   $ adb shell content query --uri content://<AuthorityPath>/search_sitemap
  *   $ adb shell content query --uri content://<AuthorityPath>/search_static
  *   $ adb shell content query --uri content://<AuthorityPath>/search_dynamic
  */
 open class EntryProvider : ContentProvider() {
     private val spaEnvironment get() = SpaEnvironmentFactory.instance
-
-    /**
-     * Enum to define all column names in provider.
-     */
-    enum class ColumnEnum(val id: String) {
-        // Columns related to page
-        PAGE_ID("pageId"),
-        PAGE_NAME("pageName"),
-        PAGE_ROUTE("pageRoute"),
-        PAGE_INTENT_URI("pageIntent"),
-        PAGE_ENTRY_COUNT("entryCount"),
-        HAS_RUNTIME_PARAM("hasRuntimeParam"),
-        PAGE_START_ADB("pageStartAdb"),
-
-        // Columns related to entry
-        ENTRY_ID("entryId"),
-        ENTRY_NAME("entryName"),
-        ENTRY_ROUTE("entryRoute"),
-        ENTRY_INTENT_URI("entryIntent"),
-        ENTRY_HIERARCHY_PATH("entryPath"),
-        ENTRY_START_ADB("entryStartAdb"),
-
-        // Columns related to search
-        ENTRY_TITLE("entryTitle"),
-        ENTRY_SEARCH_KEYWORD("entrySearchKw"),
-    }
-
-    /**
-     * Enum to define all queries supported in the provider.
-     */
-    enum class QueryEnum(
-        val queryPath: String,
-        val queryMatchCode: Int,
-        val columnNames: List<ColumnEnum>
-    ) {
-        // For debug
-        PAGE_DEBUG_QUERY(
-            "page_debug", 1,
-            listOf(ColumnEnum.PAGE_START_ADB)
-        ),
-        ENTRY_DEBUG_QUERY(
-            "entry_debug", 2,
-            listOf(ColumnEnum.ENTRY_START_ADB)
-        ),
-
-        // page related queries.
-        PAGE_INFO_QUERY(
-            "page_info", 100,
-            listOf(
-                ColumnEnum.PAGE_ID,
-                ColumnEnum.PAGE_NAME,
-                ColumnEnum.PAGE_ROUTE,
-                ColumnEnum.PAGE_INTENT_URI,
-                ColumnEnum.PAGE_ENTRY_COUNT,
-                ColumnEnum.HAS_RUNTIME_PARAM,
-            )
-        ),
-
-        // entry related queries
-        ENTRY_INFO_QUERY(
-            "entry_info", 200,
-            listOf(
-                ColumnEnum.ENTRY_ID,
-                ColumnEnum.ENTRY_NAME,
-                ColumnEnum.ENTRY_ROUTE,
-                ColumnEnum.ENTRY_INTENT_URI,
-            )
-        ),
-
-        // Search related queries
-        SEARCH_SITEMAP_QUERY(
-            "search_sitemap", 300,
-            listOf(
-                ColumnEnum.ENTRY_ID,
-                ColumnEnum.ENTRY_HIERARCHY_PATH,
-            )
-        ),
-        SEARCH_STATIC_DATA_QUERY(
-            "search_static", 301,
-            listOf(
-                ColumnEnum.ENTRY_ID,
-                ColumnEnum.ENTRY_TITLE,
-                ColumnEnum.ENTRY_SEARCH_KEYWORD,
-            )
-        ),
-        SEARCH_DYNAMIC_DATA_QUERY(
-            "search_dynamic", 302,
-            listOf(
-                ColumnEnum.ENTRY_ID,
-                ColumnEnum.ENTRY_TITLE,
-                ColumnEnum.ENTRY_SEARCH_KEYWORD,
-            )
-        ),
-    }
-
     private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
-    private fun addUri(authority: String, query: QueryEnum) {
-        uriMatcher.addURI(authority, query.queryPath, query.queryMatchCode)
-    }
 
     override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
         TODO("Implement this to handle requests to delete one or more rows")
@@ -182,13 +81,9 @@
 
     override fun attachInfo(context: Context?, info: ProviderInfo?) {
         if (info != null) {
-            addUri(info.authority, QueryEnum.PAGE_DEBUG_QUERY)
-            addUri(info.authority, QueryEnum.ENTRY_DEBUG_QUERY)
-            addUri(info.authority, QueryEnum.PAGE_INFO_QUERY)
-            addUri(info.authority, QueryEnum.ENTRY_INFO_QUERY)
-            addUri(info.authority, QueryEnum.SEARCH_SITEMAP_QUERY)
-            addUri(info.authority, QueryEnum.SEARCH_STATIC_DATA_QUERY)
-            addUri(info.authority, QueryEnum.SEARCH_DYNAMIC_DATA_QUERY)
+            QueryEnum.SEARCH_SITEMAP_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.SEARCH_STATIC_DATA_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.addUri(uriMatcher, info.authority)
         }
         super.attachInfo(context, info)
     }
@@ -202,10 +97,6 @@
     ): Cursor? {
         return try {
             when (uriMatcher.match(uri)) {
-                QueryEnum.PAGE_DEBUG_QUERY.queryMatchCode -> queryPageDebug()
-                QueryEnum.ENTRY_DEBUG_QUERY.queryMatchCode -> queryEntryDebug()
-                QueryEnum.PAGE_INFO_QUERY.queryMatchCode -> queryPageInfo()
-                QueryEnum.ENTRY_INFO_QUERY.queryMatchCode -> queryEntryInfo()
                 QueryEnum.SEARCH_SITEMAP_QUERY.queryMatchCode -> querySearchSitemap()
                 QueryEnum.SEARCH_STATIC_DATA_QUERY.queryMatchCode -> querySearchStaticData()
                 QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.queryMatchCode -> querySearchDynamicData()
@@ -219,73 +110,18 @@
         }
     }
 
-    private fun queryPageDebug(): Cursor {
-        val entryRepository by spaEnvironment.entryRepository
-        val cursor = MatrixCursor(QueryEnum.PAGE_DEBUG_QUERY.getColumns())
-        for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
-            val command = createBrowsePageAdbCommand(pageWithEntry.page)
-            if (command != null) {
-                cursor.newRow().add(ColumnEnum.PAGE_START_ADB.id, command)
-            }
-        }
-        return cursor
-    }
-
-    private fun queryEntryDebug(): Cursor {
-        val entryRepository by spaEnvironment.entryRepository
-        val cursor = MatrixCursor(QueryEnum.ENTRY_DEBUG_QUERY.getColumns())
-        for (entry in entryRepository.getAllEntries()) {
-            val command = createBrowsePageAdbCommand(entry.containerPage(), entry.id)
-            if (command != null) {
-                cursor.newRow().add(ColumnEnum.ENTRY_START_ADB.id, command)
-            }
-        }
-        return cursor
-    }
-
-    private fun queryPageInfo(): Cursor {
-        val entryRepository by spaEnvironment.entryRepository
-        val cursor = MatrixCursor(QueryEnum.PAGE_INFO_QUERY.getColumns())
-        for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
-            val page = pageWithEntry.page
-            cursor.newRow()
-                .add(ColumnEnum.PAGE_ID.id, page.id)
-                .add(ColumnEnum.PAGE_NAME.id, page.displayName)
-                .add(ColumnEnum.PAGE_ROUTE.id, page.buildRoute())
-                .add(ColumnEnum.PAGE_ENTRY_COUNT.id, pageWithEntry.entries.size)
-                .add(ColumnEnum.HAS_RUNTIME_PARAM.id, if (page.hasRuntimeParam()) 1 else 0)
-                .add(
-                    ColumnEnum.PAGE_INTENT_URI.id,
-                    createBrowsePageIntent(page).toUri(URI_INTENT_SCHEME)
-                )
-        }
-        return cursor
-    }
-
-    private fun queryEntryInfo(): Cursor {
-        val entryRepository by spaEnvironment.entryRepository
-        val cursor = MatrixCursor(QueryEnum.ENTRY_INFO_QUERY.getColumns())
-        for (entry in entryRepository.getAllEntries()) {
-            cursor.newRow()
-                .add(ColumnEnum.ENTRY_ID.id, entry.id)
-                .add(ColumnEnum.ENTRY_NAME.id, entry.displayName)
-                .add(ColumnEnum.ENTRY_ROUTE.id, entry.containerPage().buildRoute())
-                .add(
-                    ColumnEnum.ENTRY_INTENT_URI.id,
-                    createBrowsePageIntent(entry.containerPage(), entry.id).toUri(URI_INTENT_SCHEME)
-                )
-        }
-        return cursor
-    }
-
     private fun querySearchSitemap(): Cursor {
         val entryRepository by spaEnvironment.entryRepository
         val cursor = MatrixCursor(QueryEnum.SEARCH_SITEMAP_QUERY.getColumns())
         for (entry in entryRepository.getAllEntries()) {
             if (!entry.isAllowSearch) continue
+            val intent = entry.containerPage()
+                .createBrowseIntent(context, spaEnvironment.browseActivityClass, entry.id)
+                ?: Intent()
             cursor.newRow()
                 .add(ColumnEnum.ENTRY_ID.id, entry.id)
                 .add(ColumnEnum.ENTRY_HIERARCHY_PATH.id, entryRepository.getEntryPath(entry.id))
+                .add(ColumnEnum.ENTRY_INTENT_URI.id, intent.toUri(Intent.URI_INTENT_SCHEME))
         }
         return cursor
     }
@@ -321,54 +157,4 @@
                 searchData?.keyword ?: emptyList<String>()
             )
     }
-
-    private fun createBrowsePageIntent(page: SettingsPage, entryId: String? = null): Intent {
-        if (!isPageBrowsable(page)) return Intent()
-        return Intent().setComponent(ComponentName(context!!, spaEnvironment.browseActivityClass!!))
-            .apply {
-                putExtra(BrowseActivity.KEY_DESTINATION, page.buildRoute())
-                if (entryId != null) {
-                    putExtra(BrowseActivity.KEY_HIGHLIGHT_ENTRY, entryId)
-                }
-            }
-    }
-
-    private fun createBrowsePageAdbCommand(page: SettingsPage, entryId: String? = null): String? {
-        if (!isPageBrowsable(page)) return null
-        val packageName = context!!.packageName
-        val activityName = spaEnvironment.browseActivityClass!!.name.replace(packageName, "")
-        val destinationParam = " -e ${BrowseActivity.KEY_DESTINATION} ${page.buildRoute()}"
-        val highlightParam =
-            if (entryId != null) " -e ${BrowseActivity.KEY_HIGHLIGHT_ENTRY} $entryId" else ""
-        return "adb shell am start -n $packageName/$activityName$destinationParam$highlightParam"
-    }
-
-    private fun isPageBrowsable(page: SettingsPage): Boolean {
-        return context != null &&
-            spaEnvironment.browseActivityClass != null &&
-            !page.hasRuntimeParam()
-    }
-}
-
-fun EntryProvider.QueryEnum.getColumns(): Array<String> {
-    return columnNames.map { it.id }.toTypedArray()
-}
-
-fun EntryProvider.QueryEnum.getIndex(name: EntryProvider.ColumnEnum): Int {
-    return columnNames.indexOf(name)
-}
-
-fun Cursor.getString(query: EntryProvider.QueryEnum, columnName: EntryProvider.ColumnEnum): String {
-    return this.getString(query.getIndex(columnName))
-}
-
-fun Cursor.getInt(query: EntryProvider.QueryEnum, columnName: EntryProvider.ColumnEnum): Int {
-    return this.getInt(query.getIndex(columnName))
-}
-
-fun Cursor.getBoolean(
-    query: EntryProvider.QueryEnum,
-    columnName: EntryProvider.ColumnEnum
-): Boolean {
-    return this.getInt(query.getIndex(columnName)) == 1
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt
new file mode 100644
index 0000000..0707429
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.settingslib.spa.framework.common
+
+import android.content.UriMatcher
+
+/**
+ * Enum to define all column names in provider.
+ */
+enum class ColumnEnum(val id: String) {
+    // Columns related to page
+    PAGE_ID("pageId"),
+    PAGE_NAME("pageName"),
+    PAGE_ROUTE("pageRoute"),
+    PAGE_INTENT_URI("pageIntent"),
+    PAGE_ENTRY_COUNT("entryCount"),
+    HAS_RUNTIME_PARAM("hasRuntimeParam"),
+    PAGE_START_ADB("pageStartAdb"),
+
+    // Columns related to entry
+    ENTRY_ID("entryId"),
+    ENTRY_NAME("entryName"),
+    ENTRY_ROUTE("entryRoute"),
+    ENTRY_INTENT_URI("entryIntent"),
+    ENTRY_HIERARCHY_PATH("entryPath"),
+    ENTRY_START_ADB("entryStartAdb"),
+
+    // Columns related to search
+    ENTRY_TITLE("entryTitle"),
+    ENTRY_SEARCH_KEYWORD("entrySearchKw"),
+}
+
+/**
+ * Enum to define all queries supported in the provider.
+ */
+enum class QueryEnum(
+    val queryPath: String,
+    val queryMatchCode: Int,
+    val columnNames: List<ColumnEnum>
+) {
+    // For debug
+    PAGE_DEBUG_QUERY(
+        "page_debug", 1,
+        listOf(ColumnEnum.PAGE_START_ADB)
+    ),
+    ENTRY_DEBUG_QUERY(
+        "entry_debug", 2,
+        listOf(ColumnEnum.ENTRY_START_ADB)
+    ),
+
+    // page related queries.
+    PAGE_INFO_QUERY(
+        "page_info", 100,
+        listOf(
+            ColumnEnum.PAGE_ID,
+            ColumnEnum.PAGE_NAME,
+            ColumnEnum.PAGE_ROUTE,
+            ColumnEnum.PAGE_INTENT_URI,
+            ColumnEnum.PAGE_ENTRY_COUNT,
+            ColumnEnum.HAS_RUNTIME_PARAM,
+        )
+    ),
+
+    // entry related queries
+    ENTRY_INFO_QUERY(
+        "entry_info", 200,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.ENTRY_NAME,
+            ColumnEnum.ENTRY_ROUTE,
+            ColumnEnum.ENTRY_INTENT_URI,
+        )
+    ),
+
+    // Search related queries
+    SEARCH_SITEMAP_QUERY(
+        "search_sitemap", 300,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.ENTRY_HIERARCHY_PATH,
+            ColumnEnum.ENTRY_INTENT_URI,
+        )
+    ),
+    SEARCH_STATIC_DATA_QUERY(
+        "search_static", 301,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.ENTRY_TITLE,
+            ColumnEnum.ENTRY_SEARCH_KEYWORD,
+        )
+    ),
+    SEARCH_DYNAMIC_DATA_QUERY(
+        "search_dynamic", 302,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.ENTRY_TITLE,
+            ColumnEnum.ENTRY_SEARCH_KEYWORD,
+        )
+    ),
+}
+
+internal fun QueryEnum.getColumns(): Array<String> {
+    return columnNames.map { it.id }.toTypedArray()
+}
+
+internal fun QueryEnum.getIndex(name: ColumnEnum): Int {
+    return columnNames.indexOf(name)
+}
+
+internal fun QueryEnum.addUri(uriMatcher: UriMatcher, authority: String) {
+    uriMatcher.addURI(authority, queryPath, queryMatchCode)
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
index 8616b9f..fb42f01 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
@@ -37,7 +37,7 @@
 }
 
 val LocalEntryDataProvider =
-    compositionLocalOf<EntryData> { object : EntryData{} }
+    compositionLocalOf<EntryData> { object : EntryData {} }
 
 /**
  * Defines data of a Settings entry.
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
index 8f63c47..07df96e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
@@ -16,8 +16,13 @@
 
 package com.android.settingslib.spa.framework.common
 
+import android.app.Activity
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
 import android.os.Bundle
 import androidx.navigation.NamedNavArgument
+import com.android.settingslib.spa.framework.BrowseActivity
 import com.android.settingslib.spa.framework.util.isRuntimeParam
 import com.android.settingslib.spa.framework.util.navLink
 import com.android.settingslib.spa.framework.util.normalize
@@ -111,6 +116,41 @@
             details = formatDisplayTitle()
         )
     }
+
+    fun createBrowseIntent(
+        context: Context?,
+        browseActivityClass: Class<out Activity>?,
+        entryId: String? = null
+    ): Intent? {
+        if (!isBrowsable(context, browseActivityClass)) return null
+        return Intent().setComponent(ComponentName(context!!, browseActivityClass!!))
+            .apply {
+                putExtra(BrowseActivity.KEY_DESTINATION, buildRoute())
+                if (entryId != null) {
+                    putExtra(BrowseActivity.KEY_HIGHLIGHT_ENTRY, entryId)
+                }
+            }
+    }
+
+    fun createBrowseAdbCommand(
+        context: Context?,
+        browseActivityClass: Class<out Activity>?,
+        entryId: String? = null
+    ): String? {
+        if (!isBrowsable(context, browseActivityClass)) return null
+        val packageName = context!!.packageName
+        val activityName = browseActivityClass!!.name.replace(packageName, "")
+        val destinationParam = " -e ${BrowseActivity.KEY_DESTINATION} ${buildRoute()}"
+        val highlightParam =
+            if (entryId != null) " -e ${BrowseActivity.KEY_HIGHLIGHT_ENTRY} $entryId" else ""
+        return "adb shell am start -n $packageName/$activityName$destinationParam$highlightParam"
+    }
+
+    fun isBrowsable(context: Context?, browseActivityClass: Class<out Activity>?): Boolean {
+        return context != null &&
+            browseActivityClass != null &&
+            !hasRuntimeParam()
+    }
 }
 
 fun String.toHashId(): String {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
similarity index 73%
rename from packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt
rename to packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
index 6f96818..3015080 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/DebugActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
@@ -14,12 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spa.framework
+package com.android.settingslib.spa.framework.debug
 
-import android.content.Intent
-import android.net.Uri
 import android.os.Bundle
-import android.util.Log
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.compose.material3.Text
@@ -33,8 +30,6 @@
 import androidx.navigation.compose.rememberNavController
 import androidx.navigation.navArgument
 import com.android.settingslib.spa.R
-import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_DESTINATION
-import com.android.settingslib.spa.framework.BrowseActivity.Companion.KEY_HIGHLIGHT_ENTRY
 import com.android.settingslib.spa.framework.common.LogCategory
 import com.android.settingslib.spa.framework.common.SettingsEntry
 import com.android.settingslib.spa.framework.common.SettingsPage
@@ -60,11 +55,10 @@
 /**
  * The Debug Activity to display all Spa Pages & Entries.
  * One can open the debug activity by:
- *   $ adb shell am start -n <Activity>
- * For gallery, Activity = com.android.settingslib.spa.gallery/.GalleryDebugActivity
- * For SettingsGoogle, Activity = com.android.settings/.spa.SpaDebugActivity
+ *   $ adb shell am start -n <Package>/com.android.settingslib.spa.framework.debug.DebugActivity
+ * For gallery, Package = com.android.settingslib.spa.gallery
  */
-open class DebugActivity : ComponentActivity() {
+class DebugActivity : ComponentActivity() {
     private val spaEnvironment get() = SpaEnvironmentFactory.instance
 
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -79,30 +73,6 @@
         }
     }
 
-    private fun displayDebugMessage() {
-        val entryProviderAuthorities = spaEnvironment.entryProviderAuthorities ?: return
-
-        try {
-            val query = EntryProvider.QueryEnum.PAGE_INFO_QUERY
-            contentResolver.query(
-                Uri.parse("content://$entryProviderAuthorities/${query.queryPath}"),
-                null, null, null
-            ).use { cursor ->
-                while (cursor != null && cursor.moveToNext()) {
-                    val route = cursor.getString(query, EntryProvider.ColumnEnum.PAGE_ROUTE)
-                    val entryCount = cursor.getInt(query, EntryProvider.ColumnEnum.PAGE_ENTRY_COUNT)
-                    val hasRuntimeParam =
-                        cursor.getBoolean(query, EntryProvider.ColumnEnum.HAS_RUNTIME_PARAM)
-                    val message = "Page Info: $route ($entryCount) " +
-                        (if (hasRuntimeParam) "with" else "no") + "-runtime-params"
-                    spaEnvironment.logger.message(TAG, message, category = LogCategory.FRAMEWORK)
-                }
-            }
-        } catch (e: Exception) {
-            Log.e(TAG, "Provider querying exception:", e)
-        }
-    }
-
     @Composable
     private fun MainContent() {
         val navController = rememberNavController()
@@ -141,11 +111,6 @@
                 override val title = "List All Entries (${allEntry.size})"
                 override val onClick = navigator(route = ROUTE_All_ENTRIES)
             })
-            Preference(object : PreferenceModel {
-                override val title = "Query EntryProvider"
-                override val enabled = isEntryProviderAvailable().toState()
-                override val onClick = { displayDebugMessage() }
-            })
         }
     }
 
@@ -177,6 +142,7 @@
 
     @Composable
     fun OnePage(arguments: Bundle?) {
+        val context = LocalContext.current
         val entryRepository by spaEnvironment.entryRepository
         val id = arguments!!.getString(PARAM_NAME_PAGE_ID, "")
         val pageWithEntry = entryRepository.getPageWithEntry(id)!!
@@ -186,7 +152,9 @@
             Text(text = "Entry size: ${pageWithEntry.entries.size}")
             Preference(model = object : PreferenceModel {
                 override val title = "open page"
-                override val enabled = isPageClickable(pageWithEntry.page).toState()
+                override val enabled =
+                    pageWithEntry.page.isBrowsable(context, spaEnvironment.browseActivityClass)
+                        .toState()
                 override val onClick = openPage(pageWithEntry.page)
             })
             EntryList(pageWithEntry.entries)
@@ -195,6 +163,7 @@
 
     @Composable
     fun OneEntry(arguments: Bundle?) {
+        val context = LocalContext.current
         val entryRepository by spaEnvironment.entryRepository
         val id = arguments!!.getString(PARAM_NAME_ENTRY_ID, "")
         val entry = entryRepository.getEntry(id)!!
@@ -202,7 +171,9 @@
         RegularScaffold(title = "Entry - ${entry.displayTitle()}") {
             Preference(model = object : PreferenceModel {
                 override val title = "open entry"
-                override val enabled = isEntryClickable(entry).toState()
+                override val enabled =
+                    entry.containerPage().isBrowsable(context, spaEnvironment.browseActivityClass)
+                        .toState()
                 override val onClick = openEntry(entry)
             })
             Text(text = entryContent)
@@ -223,12 +194,10 @@
 
     @Composable
     private fun openPage(page: SettingsPage): (() -> Unit)? {
-        if (!isPageClickable(page)) return null
         val context = LocalContext.current
+        val intent =
+            page.createBrowseIntent(context, spaEnvironment.browseActivityClass) ?: return null
         val route = page.buildRoute()
-        val intent = Intent(context, spaEnvironment.browseActivityClass).apply {
-            putExtra(KEY_DESTINATION, route)
-        }
         return {
             spaEnvironment.logger.message(
                 TAG, "OpenPage: $route", category = LogCategory.FRAMEWORK
@@ -239,13 +208,11 @@
 
     @Composable
     private fun openEntry(entry: SettingsEntry): (() -> Unit)? {
-        if (!isEntryClickable(entry)) return null
         val context = LocalContext.current
+        val intent = entry.containerPage()
+            .createBrowseIntent(context, spaEnvironment.browseActivityClass, entry.id)
+            ?: return null
         val route = entry.containerPage().buildRoute()
-        val intent = Intent(context, spaEnvironment.browseActivityClass).apply {
-            putExtra(KEY_DESTINATION, route)
-            putExtra(KEY_HIGHLIGHT_ENTRY, entry.id)
-        }
         return {
             spaEnvironment.logger.message(
                 TAG, "OpenEntry: $route", category = LogCategory.FRAMEWORK
@@ -253,17 +220,9 @@
             context.startActivity(intent)
         }
     }
-
-    private fun isEntryProviderAvailable(): Boolean {
-        return spaEnvironment.entryProviderAuthorities != null
-    }
-
-    private fun isPageClickable(page: SettingsPage): Boolean {
-        return spaEnvironment.browseActivityClass != null && !page.hasRuntimeParam()
-    }
-
-    private fun isEntryClickable(entry: SettingsEntry): Boolean {
-        return spaEnvironment.browseActivityClass != null &&
-            !entry.containerPage().hasRuntimeParam()
-    }
 }
+
+/**
+ * A blank activity without any page.
+ */
+class BlankActivity : ComponentActivity()
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugProvider.kt
new file mode 100644
index 0000000..6c27109
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugProvider.kt
@@ -0,0 +1,176 @@
+/*
+ * 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.settingslib.spa.framework.debug
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.URI_INTENT_SCHEME
+import android.content.UriMatcher
+import android.content.pm.ProviderInfo
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.util.Log
+import com.android.settingslib.spa.framework.common.ColumnEnum
+import com.android.settingslib.spa.framework.common.QueryEnum
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.common.addUri
+import com.android.settingslib.spa.framework.common.getColumns
+
+private const val TAG = "DebugProvider"
+
+/**
+ * The content provider to return debug data.
+ * One can query the provider result by:
+ *   $ adb shell content query --uri content://<AuthorityPath>/<QueryPath>
+ * For gallery, AuthorityPath = com.android.spa.gallery.debug
+ * Some examples:
+ *   $ adb shell content query --uri content://<AuthorityPath>/page_debug
+ *   $ adb shell content query --uri content://<AuthorityPath>/entry_debug
+ *   $ adb shell content query --uri content://<AuthorityPath>/page_info
+ *   $ adb shell content query --uri content://<AuthorityPath>/entry_info
+ */
+class DebugProvider : ContentProvider() {
+    private val spaEnvironment get() = SpaEnvironmentFactory.instance
+    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
+
+    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
+        TODO("Implement this to handle requests to delete one or more rows")
+    }
+
+    override fun getType(uri: Uri): String? {
+        TODO(
+            "Implement this to handle requests for the MIME type of the data" +
+                "at the given URI"
+        )
+    }
+
+    override fun insert(uri: Uri, values: ContentValues?): Uri? {
+        TODO("Implement this to handle requests to insert a new row.")
+    }
+
+    override fun update(
+        uri: Uri,
+        values: ContentValues?,
+        selection: String?,
+        selectionArgs: Array<String>?
+    ): Int {
+        TODO("Implement this to handle requests to update one or more rows.")
+    }
+
+    override fun onCreate(): Boolean {
+        Log.d(TAG, "onCreate")
+        return true
+    }
+
+    override fun attachInfo(context: Context?, info: ProviderInfo?) {
+        if (info != null) {
+            QueryEnum.PAGE_DEBUG_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.ENTRY_DEBUG_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.PAGE_INFO_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.ENTRY_INFO_QUERY.addUri(uriMatcher, info.authority)
+        }
+        super.attachInfo(context, info)
+    }
+
+    override fun query(
+        uri: Uri,
+        projection: Array<String>?,
+        selection: String?,
+        selectionArgs: Array<String>?,
+        sortOrder: String?
+    ): Cursor? {
+        return try {
+            when (uriMatcher.match(uri)) {
+                QueryEnum.PAGE_DEBUG_QUERY.queryMatchCode -> queryPageDebug()
+                QueryEnum.ENTRY_DEBUG_QUERY.queryMatchCode -> queryEntryDebug()
+                QueryEnum.PAGE_INFO_QUERY.queryMatchCode -> queryPageInfo()
+                QueryEnum.ENTRY_INFO_QUERY.queryMatchCode -> queryEntryInfo()
+                else -> throw UnsupportedOperationException("Unknown Uri $uri")
+            }
+        } catch (e: UnsupportedOperationException) {
+            throw e
+        } catch (e: Exception) {
+            Log.e(TAG, "Provider querying exception:", e)
+            null
+        }
+    }
+
+    private fun queryPageDebug(): Cursor {
+        val entryRepository by spaEnvironment.entryRepository
+        val cursor = MatrixCursor(QueryEnum.PAGE_DEBUG_QUERY.getColumns())
+        for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
+            val command = pageWithEntry.page.createBrowseAdbCommand(
+                context,
+                spaEnvironment.browseActivityClass
+            )
+            if (command != null) {
+                cursor.newRow().add(ColumnEnum.PAGE_START_ADB.id, command)
+            }
+        }
+        return cursor
+    }
+
+    private fun queryEntryDebug(): Cursor {
+        val entryRepository by spaEnvironment.entryRepository
+        val cursor = MatrixCursor(QueryEnum.ENTRY_DEBUG_QUERY.getColumns())
+        for (entry in entryRepository.getAllEntries()) {
+            val command = entry.containerPage()
+                .createBrowseAdbCommand(context, spaEnvironment.browseActivityClass, entry.id)
+            if (command != null) {
+                cursor.newRow().add(ColumnEnum.ENTRY_START_ADB.id, command)
+            }
+        }
+        return cursor
+    }
+
+    private fun queryPageInfo(): Cursor {
+        val entryRepository by spaEnvironment.entryRepository
+        val cursor = MatrixCursor(QueryEnum.PAGE_INFO_QUERY.getColumns())
+        for (pageWithEntry in entryRepository.getAllPageWithEntry()) {
+            val page = pageWithEntry.page
+            val intent =
+                page.createBrowseIntent(context, spaEnvironment.browseActivityClass) ?: Intent()
+            cursor.newRow()
+                .add(ColumnEnum.PAGE_ID.id, page.id)
+                .add(ColumnEnum.PAGE_NAME.id, page.displayName)
+                .add(ColumnEnum.PAGE_ROUTE.id, page.buildRoute())
+                .add(ColumnEnum.PAGE_ENTRY_COUNT.id, pageWithEntry.entries.size)
+                .add(ColumnEnum.HAS_RUNTIME_PARAM.id, if (page.hasRuntimeParam()) 1 else 0)
+                .add(ColumnEnum.PAGE_INTENT_URI.id, intent.toUri(URI_INTENT_SCHEME))
+        }
+        return cursor
+    }
+
+    private fun queryEntryInfo(): Cursor {
+        val entryRepository by spaEnvironment.entryRepository
+        val cursor = MatrixCursor(QueryEnum.ENTRY_INFO_QUERY.getColumns())
+        for (entry in entryRepository.getAllEntries()) {
+            val intent = entry.containerPage()
+                .createBrowseIntent(context, spaEnvironment.browseActivityClass, entry.id)
+                ?: Intent()
+            cursor.newRow()
+                .add(ColumnEnum.ENTRY_ID.id, entry.id)
+                .add(ColumnEnum.ENTRY_NAME.id, entry.displayName)
+                .add(ColumnEnum.ENTRY_ROUTE.id, entry.containerPage().buildRoute())
+                .add(ColumnEnum.ENTRY_INTENT_URI.id, intent.toUri(URI_INTENT_SCHEME))
+        }
+        return cursor
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
index 3fa8c65..52c4893 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
@@ -44,3 +44,6 @@
 
 val ColorScheme.divider: Color
     get() = onSurface.copy(SettingsOpacity.Divider)
+
+val ColorScheme.surfaceTone: Color
+    get() = primary.copy(SettingsOpacity.SurfaceTone)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt
index 11af6ce..69ddf01 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt
@@ -20,4 +20,5 @@
     const val Full = 1f
     const val Disabled = 0.38f
     const val Divider = 0.2f
+    const val SurfaceTone = 0.14f
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
index 3e04b16..db95e23 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
@@ -28,7 +28,7 @@
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsShape
 import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.framework.util.EntryHighlight
+import com.android.settingslib.spa.widget.util.EntryHighlight
 
 @Composable
 fun MainSwitchPreference(model: SwitchPreferenceModel) {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt
index 7c0116a..8223774 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt
@@ -27,7 +27,7 @@
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.compose.stateOf
 import com.android.settingslib.spa.framework.util.WrapOnClickWithLog
-import com.android.settingslib.spa.framework.util.EntryHighlight
+import com.android.settingslib.spa.widget.util.EntryHighlight
 import com.android.settingslib.spa.widget.ui.createSettingsIcon
 
 data class SimplePreferenceMacro(
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt
index 7bca38f..4ee2af0 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt
@@ -31,8 +31,8 @@
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.framework.util.EntryHighlight
 import com.android.settingslib.spa.widget.ui.SettingsSlider
+import com.android.settingslib.spa.widget.util.EntryHighlight
 
 /**
  * The widget model for [SliderPreference] widget.
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt
index 592a99f..a76b5d7 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt
@@ -32,7 +32,7 @@
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.framework.util.WrapOnSwitchWithLog
-import com.android.settingslib.spa.framework.util.EntryHighlight
+import com.android.settingslib.spa.widget.util.EntryHighlight
 import com.android.settingslib.spa.widget.ui.SettingsSwitch
 
 /**
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt
index 63de2c8..fbfcaaa 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt
@@ -17,7 +17,7 @@
 package com.android.settingslib.spa.widget.preference
 
 import androidx.compose.runtime.Composable
-import com.android.settingslib.spa.framework.util.EntryHighlight
+import com.android.settingslib.spa.widget.util.EntryHighlight
 import com.android.settingslib.spa.widget.ui.SettingsSwitch
 
 @Composable
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt
index d8455e4..48fec3b 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt
@@ -16,13 +16,16 @@
 
 package com.android.settingslib.spa.widget.ui
 
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderDefaults
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import com.android.settingslib.spa.framework.theme.surfaceTone
 import kotlin.math.roundToInt
 
 @Composable
@@ -45,5 +48,8 @@
         valueRange = valueRange.first.toFloat()..valueRange.last.toFloat(),
         steps = if (showSteps) (valueRange.count() - 2) else 0,
         onValueChangeFinished = onValueChangeFinished,
+        colors = SliderDefaults.colors(
+            inactiveTrackColor = MaterialTheme.colorScheme.surfaceTone
+        )
     )
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryHighlight.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt
similarity index 96%
rename from packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryHighlight.kt
rename to packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt
index 8e24ce0..652e54d 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryHighlight.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spa.framework.util
+package com.android.settingslib.spa.widget.util
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
diff --git a/packages/SettingsLib/Spa/tests/Android.bp b/packages/SettingsLib/Spa/tests/Android.bp
index 1ce49fa..7491045 100644
--- a/packages/SettingsLib/Spa/tests/Android.bp
+++ b/packages/SettingsLib/Spa/tests/Android.bp
@@ -34,4 +34,5 @@
         "truth-prebuilt",
     ],
     kotlincflags: ["-Xjvm-default=all"],
+    min_sdk_version: "31",
 }
diff --git a/packages/SettingsLib/Spa/tests/AndroidManifest.xml b/packages/SettingsLib/Spa/tests/AndroidManifest.xml
index c224caf..e2db594 100644
--- a/packages/SettingsLib/Spa/tests/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/tests/AndroidManifest.xml
@@ -17,6 +17,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.settingslib.spa.tests">
 
+    <uses-sdk android:minSdkVersion="21"/>
+
     <application>
         <uses-library android:name="android.test.runner" />
     </application>
diff --git a/packages/SettingsLib/Spa/tests/build.gradle b/packages/SettingsLib/Spa/tests/build.gradle
index f950e01..b43bf18 100644
--- a/packages/SettingsLib/Spa/tests/build.gradle
+++ b/packages/SettingsLib/Spa/tests/build.gradle
@@ -21,11 +21,11 @@
 
 android {
     namespace 'com.android.settingslib.spa.tests'
-    compileSdk 33
+    compileSdk spa_target_sdk
 
     defaultConfig {
         minSdk spa_min_sdk
-        targetSdk 33
+        targetSdk spa_target_sdk
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
     }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
index 6780fb7..65d6c83 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
@@ -82,6 +82,14 @@
         /** Mutable Y coordinate of the glyph position relative from the baseline. */
         var y: Float = 0f
 
+        /**
+         * The current line of text being drawn, in a multi-line TextView.
+         */
+        var lineNo: Int = 0
+
+        /**
+         * Mutable text size of the glyph in pixels.
+         */
         /** Mutable text size of the glyph in pixels. */
         var textSize: Float = 0f
 
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
index db14fdf..f9fb42c 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
@@ -231,7 +231,9 @@
                     val origin = layout.getDrawOrigin(lineNo)
                     canvas.translate(origin, layout.getLineBaseline(lineNo).toFloat())
 
-                    run.fontRuns.forEach { fontRun -> drawFontRun(canvas, run, fontRun, tmpPaint) }
+                    run.fontRuns.forEach { fontRun ->
+                        drawFontRun(canvas, run, fontRun, lineNo, tmpPaint)
+                    }
                 } finally {
                     canvas.restore()
                 }
@@ -341,7 +343,7 @@
     var glyphFilter: GlyphCallback? = null
 
     // Draws single font run.
-    private fun drawFontRun(c: Canvas, line: Run, run: FontRun, paint: Paint) {
+    private fun drawFontRun(c: Canvas, line: Run, run: FontRun, lineNo: Int, paint: Paint) {
         var arrayIndex = 0
         val font = fontInterpolator.lerp(run.baseFont, run.targetFont, progress)
 
@@ -360,11 +362,13 @@
         tmpGlyph.font = font
         tmpGlyph.runStart = run.start
         tmpGlyph.runLength = run.end - run.start
+        tmpGlyph.lineNo = lineNo
 
         tmpPaintForGlyph.set(paint)
         var prevStart = run.start
 
         for (i in run.start until run.end) {
+            tmpGlyph.glyphIndex = i
             tmpGlyph.glyphId = line.glyphIds[i]
             tmpGlyph.x = MathUtils.lerp(line.baseX[i], line.targetX[i], progress)
             tmpGlyph.y = MathUtils.lerp(line.baseY[i], line.targetY[i], progress)
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt
new file mode 100644
index 0000000..1db0725
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+
+private const val CLASS_SETTINGS = "android.provider.Settings"
+
+/**
+ * Detects usage of static methods in android.provider.Settings and suggests to use an injected
+ * settings provider instance instead.
+ */
+@Suppress("UnstableApiUsage")
+class StaticSettingsProviderDetector : Detector(), SourceCodeScanner {
+    override fun getApplicableMethodNames(): List<String> {
+        return listOf(
+            "getFloat",
+            "getInt",
+            "getLong",
+            "getString",
+            "getUriFor",
+            "putFloat",
+            "putInt",
+            "putLong",
+            "putString"
+        )
+    }
+
+    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+        val evaluator = context.evaluator
+        val className = method.containingClass?.qualifiedName
+        if (
+            className != "$CLASS_SETTINGS.Global" &&
+                className != "$CLASS_SETTINGS.Secure" &&
+                className != "$CLASS_SETTINGS.System"
+        ) {
+            return
+        }
+        if (!evaluator.isStatic(method)) {
+            return
+        }
+
+        val subclassName = className.substring(CLASS_SETTINGS.length + 1)
+
+        context.report(
+            ISSUE,
+            method,
+            context.getNameLocation(node),
+            "`@Inject` a ${subclassName}Settings instead"
+        )
+    }
+
+    companion object {
+        @JvmField
+        val ISSUE: Issue =
+            Issue.create(
+                id = "StaticSettingsProvider",
+                briefDescription = "Static settings provider usage",
+                explanation =
+                    """
+                    Static settings provider methods, such as `Settings.Global.putInt()`, should \
+                    not be used because they make testing difficult. Instead, use an injected \
+                    settings provider. For example, instead of calling `Settings.Secure.getInt()`, \
+                    annotate the class constructor with `@Inject` and add `SecureSettings` to the \
+                    parameters.
+                    """,
+                category = Category.CORRECTNESS,
+                priority = 8,
+                severity = Severity.WARNING,
+                implementation =
+                    Implementation(
+                        StaticSettingsProviderDetector::class.java,
+                        Scope.JAVA_FILE_SCOPE
+                    )
+            )
+    }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
index cf7c1b5..3f334c1c 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
@@ -36,6 +36,7 @@
                 RegisterReceiverViaContextDetector.ISSUE,
                 SoftwareBitmapDetector.ISSUE,
                 NonInjectedServiceDetector.ISSUE,
+                StaticSettingsProviderDetector.ISSUE
         )
 
     override val api: Int
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
index 486af9d..d4c55c0 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
@@ -24,6 +24,47 @@
 @NonNull
 private fun indentedJava(@NonNull @Language("JAVA") source: String) = java(source).indented()
 
+internal val commonSettingsCode =
+    """
+public static float getFloat(ContentResolver cr, String name) { return 0.0f; }
+public static long getLong(ContentResolver cr, String name) {
+    return 0L;
+}
+public static int getInt(ContentResolver cr, String name) {
+    return 0;
+}
+public static String getString(ContentResolver cr, String name) {
+    return "";
+}
+public static float getFloat(ContentResolver cr, String name, float def) {
+    return 0.0f;
+}
+public static long getLong(ContentResolver cr, String name, long def) {
+    return 0L;
+}
+public static int getInt(ContentResolver cr, String name, int def) {
+    return 0;
+}
+public static String getString(ContentResolver cr, String name, String def) {
+    return "";
+}
+public static boolean putFloat(ContentResolver cr, String name, float value) {
+    return true;
+}
+public static boolean putLong(ContentResolver cr, String name, long value) {
+    return true;
+}
+public static boolean putInt(ContentResolver cr, String name, int value) {
+    return true;
+}
+public static boolean putFloat(ContentResolver cr, String name) {
+    return true;
+}
+public static boolean putString(ContentResolver cr, String name, String value) {
+    return true;
+}
+"""
+
 /*
  * This file contains stubs of framework APIs and System UI classes for testing purposes only. The
  * stubs are not used in the lint detectors themselves.
@@ -186,4 +227,28 @@
 }
 """
         ),
+        indentedJava(
+            """
+package android.provider;
+
+public class Settings {
+    public static final class Global {
+        public static final String UNLOCK_SOUND = "unlock_sound";
+        """ +
+                commonSettingsCode +
+                """
+    }
+    public static final class Secure {
+    """ +
+                commonSettingsCode +
+                """
+    }
+    public static final class System {
+    """ +
+                commonSettingsCode +
+                """
+    }
+}
+"""
+        ),
     )
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
index 6ae8fd3..c35ac61 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class BindServiceOnMainThreadDetectorTest : LintDetectorTest() {
+class BindServiceOnMainThreadDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = BindServiceOnMainThreadDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(BindServiceOnMainThreadDetector.ISSUE)
 
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
index 7d42280..376acb5 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class BroadcastSentViaContextDetectorTest : LintDetectorTest() {
+class BroadcastSentViaContextDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = BroadcastSentViaContextDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(BroadcastSentViaContextDetector.ISSUE)
 
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
index c468af8..301c338 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class NonInjectedMainThreadDetectorTest : LintDetectorTest() {
+class NonInjectedMainThreadDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = NonInjectedMainThreadDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(NonInjectedMainThreadDetector.ISSUE)
 
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
index c83a35b..0a74bfc 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class NonInjectedServiceDetectorTest : LintDetectorTest() {
+class NonInjectedServiceDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = NonInjectedServiceDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
     override fun getIssues(): List<Issue> = listOf(NonInjectedServiceDetector.ISSUE)
 
     @Test
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
index ebcddeb..9ed7aa0 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class RegisterReceiverViaContextDetectorTest : LintDetectorTest() {
+class RegisterReceiverViaContextDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = RegisterReceiverViaContextDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(RegisterReceiverViaContextDetector.ISSUE)
 
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
index b03a11c..54cac7b 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class SlowUserQueryDetectorTest : LintDetectorTest() {
+class SlowUserQueryDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = SlowUserQueryDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> =
         listOf(
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
index fb6537e..090ddf8 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class SoftwareBitmapDetectorTest : LintDetectorTest() {
+class SoftwareBitmapDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = SoftwareBitmapDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(SoftwareBitmapDetector.ISSUE)
 
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt
new file mode 100644
index 0000000..b83ed70
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt
@@ -0,0 +1,208 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+@Suppress("UnstableApiUsage")
+class StaticSettingsProviderDetectorTest : SystemUILintDetectorTest() {
+
+    override fun getDetector(): Detector = StaticSettingsProviderDetector()
+    override fun getIssues(): List<Issue> = listOf(StaticSettingsProviderDetector.ISSUE)
+
+    @Test
+    fun testGetServiceWithString() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                        package test.pkg;
+
+                        import android.provider.Settings;
+                        import android.provider.Settings.Global;
+                        import android.provider.Settings.Secure;
+
+                        public class TestClass {
+                            public void getSystemServiceWithoutDagger(Context context) {
+                                final ContentResolver cr = mContext.getContentResolver();
+                                Global.getFloat(cr, Settings.Global.UNLOCK_SOUND);
+                                Global.getInt(cr, Settings.Global.UNLOCK_SOUND);
+                                Global.getLong(cr, Settings.Global.UNLOCK_SOUND);
+                                Global.getString(cr, Settings.Global.UNLOCK_SOUND);
+                                Global.getFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+                                Global.getInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+                                Global.getLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+                                Global.getString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                                Global.putFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+                                Global.putInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+                                Global.putLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+                                Global.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+
+                                Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                                Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                                Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                                Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                                Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+                                Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+                                Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+                                Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+                                Secure.putFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+                                Secure.putInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+                                Secure.putLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+                                Secure.putString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+
+                                Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+                                Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+                                Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+                                Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT, "1");
+                                Settings.System.putFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+                                Settings.System.putInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+                                Settings.System.putLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+                                Settings.System.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                            }
+                        }
+                        """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(StaticSettingsProviderDetector.ISSUE)
+            .run()
+            .expect(
+                """
+                src/test/pkg/TestClass.java:10: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getFloat(cr, Settings.Global.UNLOCK_SOUND);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:11: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getInt(cr, Settings.Global.UNLOCK_SOUND);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:12: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getLong(cr, Settings.Global.UNLOCK_SOUND);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:13: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getString(cr, Settings.Global.UNLOCK_SOUND);
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:14: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:15: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:16: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:17: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:18: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.putFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:19: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.putInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:20: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.putLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:21: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:23: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:24: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:25: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:26: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:27: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:28: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:29: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:30: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:31: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.putFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:32: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.putInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:33: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.putLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:34: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.putString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:36: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                        ~~~~~~~~
+                src/test/pkg/TestClass.java:37: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                        ~~~~~~
+                src/test/pkg/TestClass.java:38: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                        ~~~~~~~
+                src/test/pkg/TestClass.java:39: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                        ~~~~~~~~~
+                src/test/pkg/TestClass.java:40: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+                                        ~~~~~~~~
+                src/test/pkg/TestClass.java:41: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+                                        ~~~~~~
+                src/test/pkg/TestClass.java:42: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+                                        ~~~~~~~
+                src/test/pkg/TestClass.java:43: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT, "1");
+                                        ~~~~~~~~~
+                src/test/pkg/TestClass.java:44: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.putFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+                                        ~~~~~~~~
+                src/test/pkg/TestClass.java:45: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.putInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+                                        ~~~~~~
+                src/test/pkg/TestClass.java:46: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.putLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+                                        ~~~~~~~
+                src/test/pkg/TestClass.java:47: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                                        ~~~~~~~~~
+                0 errors, 36 warnings
+                """
+            )
+    }
+
+    private val stubs = androidStubs
+}
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt
new file mode 100644
index 0000000..2183b38
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt
@@ -0,0 +1,15 @@
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import java.io.File
+
+@Suppress("UnstableApiUsage")
+abstract class SystemUILintDetectorTest : LintDetectorTest() {
+    /**
+     * Customize the lint task to disable SDK usage completely. This ensures that running the tests
+     * in Android Studio has the same result as running the tests in atest
+     */
+    override fun lint(): TestLintTask =
+        super.lint().allowMissingSdk(true).sdkHome(File("/dev/null"))
+}
diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp
index cafaaf8..7709f21 100644
--- a/packages/SystemUI/plugin/Android.bp
+++ b/packages/SystemUI/plugin/Android.bp
@@ -33,6 +33,7 @@
 
     static_libs: [
         "androidx.annotation_annotation",
+        "error_prone_annotations",
         "PluginCoreLib",
         "SystemUIAnimationLib",
     ],
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
index 1e74c3d..dabb43b 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
@@ -14,6 +14,7 @@
 package com.android.systemui.plugins
 
 import android.content.res.Resources
+import android.graphics.Rect
 import android.graphics.drawable.Drawable
 import android.view.View
 import com.android.systemui.plugins.annotations.ProvidesInterface
@@ -114,6 +115,17 @@
 
     /** Runs the battery animation (if any). */
     fun charge() { }
+
+    /** Move the clock, for example, if the notification tray appears in split-shade mode. */
+    fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) { }
+
+    /**
+     * Whether this clock has a custom position update animation. If true, the keyguard will call
+     * `onPositionUpdated` to notify the clock of a position update animation. If false, a default
+     * animation will be used (e.g. a simple translation).
+     */
+    val hasCustomPositionUpdatedAnimation
+        get() = false
 }
 
 /** Events that have specific data about the related face */
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
similarity index 85%
rename from packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
index 6124e10..6436dcb 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt
@@ -14,12 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.log
+package com.android.systemui.plugins.log
 
 import android.os.Trace
 import android.util.Log
-import com.android.systemui.log.dagger.LogModule
-import com.android.systemui.util.collection.RingBuffer
+import com.android.systemui.plugins.util.RingBuffer
 import com.google.errorprone.annotations.CompileTimeConstant
 import java.io.PrintWriter
 import java.util.concurrent.ArrayBlockingQueue
@@ -61,15 +60,18 @@
  * In either case, `level` can be any of `verbose`, `debug`, `info`, `warn`, `error`, `assert`, or
  * the first letter of any of the previous.
  *
- * Buffers are provided by [LogModule]. Instances should be created using a [LogBufferFactory].
+ * In SystemUI, buffers are provided by LogModule. Instances should be created using a SysUI
+ * LogBufferFactory.
  *
  * @param name The name of this buffer, printed when the buffer is dumped and in some other
  * situations.
  * @param maxSize The maximum number of messages to keep in memory at any one time. Buffers start
- * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches
- * the maximum, it behaves like a ring buffer.
+ * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches the
+ * maximum, it behaves like a ring buffer.
  */
-class LogBuffer @JvmOverloads constructor(
+class LogBuffer
+@JvmOverloads
+constructor(
     private val name: String,
     private val maxSize: Int,
     private val logcatEchoTracker: LogcatEchoTracker,
@@ -78,7 +80,7 @@
     private val buffer = RingBuffer(maxSize) { LogMessageImpl.create() }
 
     private val echoMessageQueue: BlockingQueue<LogMessage>? =
-            if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null
+        if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null
 
     init {
         if (logcatEchoTracker.logInBackgroundThread && echoMessageQueue != null) {
@@ -133,11 +135,11 @@
      */
     @JvmOverloads
     inline fun log(
-            tag: String,
-            level: LogLevel,
-            messageInitializer: MessageInitializer,
-            noinline messagePrinter: MessagePrinter,
-            exception: Throwable? = null,
+        tag: String,
+        level: LogLevel,
+        messageInitializer: MessageInitializer,
+        noinline messagePrinter: MessagePrinter,
+        exception: Throwable? = null,
     ) {
         val message = obtain(tag, level, messagePrinter, exception)
         messageInitializer(message)
@@ -152,14 +154,13 @@
      * log message is built during runtime, use the [LogBuffer.log] overloaded method that takes in
      * an initializer and a message printer.
      *
-     * Log buffers are limited by the number of entries, so logging more frequently
-     * will limit the time window that the LogBuffer covers in a bug report.  Richer logs, on the
-     * other hand, make a bug report more actionable, so using the [log] with a messagePrinter to
-     * add more detail to every log may do more to improve overall logging than adding more logs
-     * with this method.
+     * Log buffers are limited by the number of entries, so logging more frequently will limit the
+     * time window that the LogBuffer covers in a bug report. Richer logs, on the other hand, make a
+     * bug report more actionable, so using the [log] with a messagePrinter to add more detail to
+     * every log may do more to improve overall logging than adding more logs with this method.
      */
     fun log(tag: String, level: LogLevel, @CompileTimeConstant message: String) =
-            log(tag, level, {str1 = message}, { str1!! })
+        log(tag, level, { str1 = message }, { str1!! })
 
     /**
      * You should call [log] instead of this method.
@@ -172,10 +173,10 @@
      */
     @Synchronized
     fun obtain(
-            tag: String,
-            level: LogLevel,
-            messagePrinter: MessagePrinter,
-            exception: Throwable? = null,
+        tag: String,
+        level: LogLevel,
+        messagePrinter: MessagePrinter,
+        exception: Throwable? = null,
     ): LogMessage {
         if (!mutable) {
             return FROZEN_MESSAGE
@@ -189,8 +190,7 @@
      * You should call [log] instead of this method.
      *
      * After acquiring a message via [obtain], call this method to signal to the buffer that you
-     * have finished filling in its data fields. The message will be echoed to logcat if
-     * necessary.
+     * have finished filling in its data fields. The message will be echoed to logcat if necessary.
      */
     @Synchronized
     fun commit(message: LogMessage) {
@@ -213,7 +213,8 @@
 
     /** Sends message to echo after determining whether to use Logcat and/or systrace. */
     private fun echoToDesiredEndpoints(message: LogMessage) {
-        val includeInLogcat = logcatEchoTracker.isBufferLoggable(name, message.level) ||
+        val includeInLogcat =
+            logcatEchoTracker.isBufferLoggable(name, message.level) ||
                 logcatEchoTracker.isTagLoggable(message.tag, message.level)
         echo(message, toLogcat = includeInLogcat, toSystrace = systrace)
     }
@@ -221,7 +222,12 @@
     /** Converts the entire buffer to a newline-delimited string */
     @Synchronized
     fun dump(pw: PrintWriter, tailLength: Int) {
-        val iterationStart = if (tailLength <= 0) { 0 } else { max(0, buffer.size - tailLength) }
+        val iterationStart =
+            if (tailLength <= 0) {
+                0
+            } else {
+                max(0, buffer.size - tailLength)
+            }
 
         for (i in iterationStart until buffer.size) {
             buffer[i].dump(pw)
@@ -229,9 +235,9 @@
     }
 
     /**
-     * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called.
-     * Calls to [log], [obtain], and [commit] will not affect the buffer and will return dummy
-     * values if necessary.
+     * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called. Calls
+     * to [log], [obtain], and [commit] will not affect the buffer and will return dummy values if
+     * necessary.
      */
     @Synchronized
     fun freeze() {
@@ -241,9 +247,7 @@
         }
     }
 
-    /**
-     * Undoes the effects of calling [freeze].
-     */
+    /** Undoes the effects of calling [freeze]. */
     @Synchronized
     fun unfreeze() {
         if (frozen) {
@@ -265,8 +269,11 @@
     }
 
     private fun echoToSystrace(message: LogMessage, strMessage: String) {
-        Trace.instantForTrack(Trace.TRACE_TAG_APP, "UI Events",
-            "$name - ${message.level.shortString} ${message.tag}: $strMessage")
+        Trace.instantForTrack(
+            Trace.TRACE_TAG_APP,
+            "UI Events",
+            "$name - ${message.level.shortString} ${message.tag}: $strMessage"
+        )
     }
 
     private fun echoToLogcat(message: LogMessage, strMessage: String) {
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogLevel.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt
similarity index 83%
rename from packages/SystemUI/src/com/android/systemui/log/LogLevel.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt
index 53f231c..b036cf0 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogLevel.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogLevel.kt
@@ -14,17 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.systemui.log
+package com.android.systemui.plugins.log
 
 import android.util.Log
 
-/**
- * Enum version of @Log.Level
- */
-enum class LogLevel(
-    @Log.Level val nativeLevel: Int,
-    val shortString: String
-) {
+/** Enum version of @Log.Level */
+enum class LogLevel(@Log.Level val nativeLevel: Int, val shortString: String) {
     VERBOSE(Log.VERBOSE, "V"),
     DEBUG(Log.DEBUG, "D"),
     INFO(Log.INFO, "I"),
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt
similarity index 75%
rename from packages/SystemUI/src/com/android/systemui/log/LogMessage.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt
index dae2592..9468681 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessage.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.log
+package com.android.systemui.plugins.log
 
 import java.io.PrintWriter
 import java.text.SimpleDateFormat
@@ -29,9 +29,10 @@
  *
  * When a message is logged, the code doing the logging stores data in one or more of the generic
  * fields ([str1], [int1], etc). When it comes time to dump the message to logcat/bugreport/etc, the
- * [messagePrinter] function reads the data stored in the generic fields and converts that to a human-
- * readable string. Thus, for every log type there must be a specialized initializer function that
- * stores data specific to that log type and a specialized printer function that prints that data.
+ * [messagePrinter] function reads the data stored in the generic fields and converts that to a
+ * human- readable string. Thus, for every log type there must be a specialized initializer function
+ * that stores data specific to that log type and a specialized printer function that prints that
+ * data.
  *
  * See [LogBuffer.log] for more information.
  */
@@ -55,9 +56,7 @@
     var bool3: Boolean
     var bool4: Boolean
 
-    /**
-     * Function that dumps the [LogMessage] to the provided [writer].
-     */
+    /** Function that dumps the [LogMessage] to the provided [writer]. */
     fun dump(writer: PrintWriter) {
         val formattedTimestamp = DATE_FORMAT.format(timestamp)
         val shortLevel = level.shortString
@@ -68,12 +67,12 @@
 }
 
 /**
- * A function that will be called if and when the message needs to be dumped to
- * logcat or a bug report. It should read the data stored by the initializer and convert it to
- * a human-readable string. The value of `this` will be the LogMessage to be printed.
- * **IMPORTANT:** The printer should ONLY ever reference fields on the LogMessage and NEVER any
- * variables in its enclosing scope. Otherwise, the runtime will need to allocate a new instance
- * of the printer for each call, thwarting our attempts at avoiding any sort of allocation.
+ * A function that will be called if and when the message needs to be dumped to logcat or a bug
+ * report. It should read the data stored by the initializer and convert it to a human-readable
+ * string. The value of `this` will be the LogMessage to be printed. **IMPORTANT:** The printer
+ * should ONLY ever reference fields on the LogMessage and NEVER any variables in its enclosing
+ * scope. Otherwise, the runtime will need to allocate a new instance of the printer for each call,
+ * thwarting our attempts at avoiding any sort of allocation.
  */
 typealias MessagePrinter = LogMessage.() -> String
 
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt
index 4dd6f65..f2a6a91 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogMessageImpl.kt
@@ -14,11 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.systemui.log
+package com.android.systemui.plugins.log
 
-/**
- * Recyclable implementation of [LogMessage].
- */
+/** Recyclable implementation of [LogMessage]. */
 data class LogMessageImpl(
     override var level: LogLevel,
     override var tag: String,
@@ -68,23 +66,24 @@
     companion object Factory {
         fun create(): LogMessageImpl {
             return LogMessageImpl(
-                    LogLevel.DEBUG,
-                    DEFAULT_TAG,
-                    0,
-                    DEFAULT_PRINTER,
-                    null,
-                    null,
-                    null,
-                    null,
-                    0,
-                    0,
-                    0,
-                    0,
-                    0.0,
-                    false,
-                    false,
-                    false,
-                    false)
+                LogLevel.DEBUG,
+                DEFAULT_TAG,
+                0,
+                DEFAULT_PRINTER,
+                null,
+                null,
+                null,
+                null,
+                0,
+                0,
+                0,
+                0,
+                0.0,
+                false,
+                false,
+                false,
+                false
+            )
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt
similarity index 68%
rename from packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt
index 8cda423..cfe894f 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTracker.kt
@@ -14,24 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.systemui.log
+package com.android.systemui.plugins.log
 
-/**
- * Keeps track of which [LogBuffer] messages should also appear in logcat.
- */
+/** Keeps track of which [LogBuffer] messages should also appear in logcat. */
 interface LogcatEchoTracker {
-    /**
-     * Whether [bufferName] should echo messages of [level] or higher to logcat.
-     */
+    /** Whether [bufferName] should echo messages of [level] or higher to logcat. */
     fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean
 
-    /**
-     * Whether [tagName] should echo messages of [level] or higher to logcat.
-     */
+    /** Whether [tagName] should echo messages of [level] or higher to logcat. */
     fun isTagLoggable(tagName: String, level: LogLevel): Boolean
 
-    /**
-     * Whether to log messages in a background thread.
-     */
+    /** Whether to log messages in a background thread. */
     val logInBackgroundThread: Boolean
 }
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
similarity index 73%
rename from packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
index 40b0cdc..d3fabac 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerDebug.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.log
+package com.android.systemui.plugins.log
 
 import android.content.ContentResolver
 import android.database.ContentObserver
@@ -36,19 +36,15 @@
  * $ adb shell settings put global systemui/tag/<tag> <level>
  * ```
  */
-class LogcatEchoTrackerDebug private constructor(
-    private val contentResolver: ContentResolver
-) : LogcatEchoTracker {
+class LogcatEchoTrackerDebug private constructor(private val contentResolver: ContentResolver) :
+    LogcatEchoTracker {
     private val cachedBufferLevels: MutableMap<String, LogLevel> = mutableMapOf()
     private val cachedTagLevels: MutableMap<String, LogLevel> = mutableMapOf()
     override val logInBackgroundThread = true
 
     companion object Factory {
         @JvmStatic
-        fun create(
-            contentResolver: ContentResolver,
-            mainLooper: Looper
-        ): LogcatEchoTrackerDebug {
+        fun create(contentResolver: ContentResolver, mainLooper: Looper): LogcatEchoTrackerDebug {
             val tracker = LogcatEchoTrackerDebug(contentResolver)
             tracker.attach(mainLooper)
             return tracker
@@ -57,37 +53,35 @@
 
     private fun attach(mainLooper: Looper) {
         contentResolver.registerContentObserver(
-                Settings.Global.getUriFor(BUFFER_PATH),
-                true,
-                object : ContentObserver(Handler(mainLooper)) {
-                    override fun onChange(selfChange: Boolean, uri: Uri?) {
-                        super.onChange(selfChange, uri)
-                        cachedBufferLevels.clear()
-                    }
-                })
+            Settings.Global.getUriFor(BUFFER_PATH),
+            true,
+            object : ContentObserver(Handler(mainLooper)) {
+                override fun onChange(selfChange: Boolean, uri: Uri?) {
+                    super.onChange(selfChange, uri)
+                    cachedBufferLevels.clear()
+                }
+            }
+        )
 
         contentResolver.registerContentObserver(
-                Settings.Global.getUriFor(TAG_PATH),
-                true,
-                object : ContentObserver(Handler(mainLooper)) {
-                    override fun onChange(selfChange: Boolean, uri: Uri?) {
-                        super.onChange(selfChange, uri)
-                        cachedTagLevels.clear()
-                    }
-                })
+            Settings.Global.getUriFor(TAG_PATH),
+            true,
+            object : ContentObserver(Handler(mainLooper)) {
+                override fun onChange(selfChange: Boolean, uri: Uri?) {
+                    super.onChange(selfChange, uri)
+                    cachedTagLevels.clear()
+                }
+            }
+        )
     }
 
-    /**
-     * Whether [bufferName] should echo messages of [level] or higher to logcat.
-     */
+    /** Whether [bufferName] should echo messages of [level] or higher to logcat. */
     @Synchronized
     override fun isBufferLoggable(bufferName: String, level: LogLevel): Boolean {
         return level.ordinal >= getLogLevel(bufferName, BUFFER_PATH, cachedBufferLevels).ordinal
     }
 
-    /**
-     * Whether [tagName] should echo messages of [level] or higher to logcat.
-     */
+    /** Whether [tagName] should echo messages of [level] or higher to logcat. */
     @Synchronized
     override fun isTagLoggable(tagName: String, level: LogLevel): Boolean {
         return level >= getLogLevel(tagName, TAG_PATH, cachedTagLevels)
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt
similarity index 89%
rename from packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt
index 1a4ad19..3c8bda4 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogcatEchoTrackerProd.kt
@@ -14,11 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.systemui.log
+package com.android.systemui.plugins.log
 
-/**
- * Production version of [LogcatEchoTracker] that isn't configurable.
- */
+/** Production version of [LogcatEchoTracker] that isn't configurable. */
 class LogcatEchoTrackerProd : LogcatEchoTracker {
     override val logInBackgroundThread = false
 
diff --git a/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt
similarity index 82%
rename from packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt
rename to packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt
index 97dc842..68d7890 100644
--- a/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/util/RingBuffer.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.util.collection
+package com.android.systemui.plugins.util
 
 import kotlin.math.max
 
@@ -32,19 +32,16 @@
  * @param factory A function that creates a fresh instance of T. Used by the buffer while it's
  * growing to [maxSize].
  */
-class RingBuffer<T>(
-    private val maxSize: Int,
-    private val factory: () -> T
-) : Iterable<T> {
+class RingBuffer<T>(private val maxSize: Int, private val factory: () -> T) : Iterable<T> {
 
     private val buffer = MutableList<T?>(maxSize) { null }
 
     /**
      * An abstract representation that points to the "end" of the buffer. Increments every time
-     * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into
-     * the backing array. Always points to the "next" available slot in the buffer. Before the
-     * buffer has completely filled, the value pointed to will be null. Afterward, it will be the
-     * value at the "beginning" of the buffer.
+     * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into the
+     * backing array. Always points to the "next" available slot in the buffer. Before the buffer
+     * has completely filled, the value pointed to will be null. Afterward, it will be the value at
+     * the "beginning" of the buffer.
      *
      * This value is unlikely to overflow. Assuming [advance] is called at rate of 100 calls/ms,
      * omega will overflow after a little under three million years of continuous operation.
@@ -60,24 +57,23 @@
 
     /**
      * Advances the buffer's position by one and returns the value that is now present at the "end"
-     * of the buffer. If the buffer is not yet full, uses [factory] to create a new item.
-     * Otherwise, reuses the value that was previously at the "beginning" of the buffer.
+     * of the buffer. If the buffer is not yet full, uses [factory] to create a new item. Otherwise,
+     * reuses the value that was previously at the "beginning" of the buffer.
      *
-     * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that
-     * was previously stored on it.
+     * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that was
+     * previously stored on it.
      */
     fun advance(): T {
         val index = indexOf(omega)
         omega += 1
-        val entry = buffer[index] ?: factory().also {
-            buffer[index] = it
-        }
+        val entry = buffer[index] ?: factory().also { buffer[index] = it }
         return entry
     }
 
     /**
      * Returns the value stored at [index], which can range from 0 (the "start", or oldest element
-     * of the buffer) to [size] - 1 (the "end", or newest element of the buffer).
+     * of the buffer) to [size]
+     * - 1 (the "end", or newest element of the buffer).
      */
     operator fun get(index: Int): T {
         if (index < 0 || index >= size) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt b/packages/SystemUI/plugin/tests/log/LogBufferTest.kt
similarity index 74%
rename from packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt
rename to packages/SystemUI/plugin/tests/log/LogBufferTest.kt
index 56aff3c..a39b856 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt
+++ b/packages/SystemUI/plugin/tests/log/LogBufferTest.kt
@@ -2,6 +2,7 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.log.LogBuffer
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
 import java.io.StringWriter
@@ -18,8 +19,7 @@
 
     private lateinit var outputWriter: StringWriter
 
-    @Mock
-    private lateinit var logcatEchoTracker: LogcatEchoTracker
+    @Mock private lateinit var logcatEchoTracker: LogcatEchoTracker
 
     @Before
     fun setup() {
@@ -67,15 +67,17 @@
     @Test
     fun dump_writesCauseAndStacktrace() {
         buffer = createBuffer()
-        val exception = createTestException("Exception message",
+        val exception =
+            createTestException(
+                "Exception message",
                 "TestClass",
-                cause = createTestException("The real cause!", "TestClass"))
+                cause = createTestException("The real cause!", "TestClass")
+            )
         buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
 
         val dumpedString = dumpBuffer()
 
-        assertThat(dumpedString)
-                .contains("Caused by: java.lang.RuntimeException: The real cause!")
+        assertThat(dumpedString).contains("Caused by: java.lang.RuntimeException: The real cause!")
         assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:1)")
         assertThat(dumpedString).contains("at TestClass.TestMethod(TestClass.java:2)")
     }
@@ -85,49 +87,47 @@
         buffer = createBuffer()
         val exception = RuntimeException("Root exception message")
         exception.addSuppressed(
-                createTestException(
-                        "First suppressed exception",
-                        "FirstClass",
-                        createTestException("Cause of suppressed exp", "ThirdClass")
-                ))
-        exception.addSuppressed(
-                createTestException("Second suppressed exception", "SecondClass"))
+            createTestException(
+                "First suppressed exception",
+                "FirstClass",
+                createTestException("Cause of suppressed exp", "ThirdClass")
+            )
+        )
+        exception.addSuppressed(createTestException("Second suppressed exception", "SecondClass"))
         buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
 
         val dumpedStr = dumpBuffer()
 
         // first suppressed exception
         assertThat(dumpedStr)
-                .contains("Suppressed: " +
-                        "java.lang.RuntimeException: First suppressed exception")
+            .contains("Suppressed: " + "java.lang.RuntimeException: First suppressed exception")
         assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:1)")
         assertThat(dumpedStr).contains("at FirstClass.TestMethod(FirstClass.java:2)")
 
         assertThat(dumpedStr)
-                .contains("Caused by: java.lang.RuntimeException: Cause of suppressed exp")
+            .contains("Caused by: java.lang.RuntimeException: Cause of suppressed exp")
         assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:1)")
         assertThat(dumpedStr).contains("at ThirdClass.TestMethod(ThirdClass.java:2)")
 
         // second suppressed exception
         assertThat(dumpedStr)
-                .contains("Suppressed: " +
-                        "java.lang.RuntimeException: Second suppressed exception")
+            .contains("Suppressed: " + "java.lang.RuntimeException: Second suppressed exception")
         assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:1)")
         assertThat(dumpedStr).contains("at SecondClass.TestMethod(SecondClass.java:2)")
     }
 
     private fun createTestException(
-            message: String,
-            errorClass: String,
-            cause: Throwable? = null,
+        message: String,
+        errorClass: String,
+        cause: Throwable? = null,
     ): Exception {
         val exception = RuntimeException(message, cause)
-        exception.stackTrace = (1..5).map { lineNumber ->
-            StackTraceElement(errorClass,
-                    "TestMethod",
-                    "$errorClass.java",
-                    lineNumber)
-        }.toTypedArray()
+        exception.stackTrace =
+            (1..5)
+                .map { lineNumber ->
+                    StackTraceElement(errorClass, "TestMethod", "$errorClass.java", lineNumber)
+                }
+                .toTypedArray()
         return exception
     }
 
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
index 3ad7c8c..d64587d 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
@@ -37,6 +37,7 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:layout_marginTop="@dimen/keyguard_large_clock_top_margin"
+        android:clipChildren="false"
         android:visibility="gone" />
 
     <!-- Not quite optimal but needed to translate these items as a group. The
diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml
index 5dc34b9..a565988 100644
--- a/packages/SystemUI/res/layout/combined_qs_header.xml
+++ b/packages/SystemUI/res/layout/combined_qs_header.xml
@@ -73,8 +73,8 @@
         android:singleLine="true"
         android:textDirection="locale"
         android:textAppearance="@style/TextAppearance.QS.Status"
-        android:transformPivotX="0sp"
-        android:transformPivotY="20sp"
+        android:transformPivotX="0dp"
+        android:transformPivotY="24dp"
         android:scaleX="1"
         android:scaleY="1"
     />
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 01c9ac1..66f0e75 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -519,7 +519,7 @@
     <dimen name="qs_tile_margin_horizontal">8dp</dimen>
     <dimen name="qs_tile_margin_vertical">@dimen/qs_tile_margin_horizontal</dimen>
     <dimen name="qs_tile_margin_top_bottom">4dp</dimen>
-    <dimen name="qs_brightness_margin_top">8dp</dimen>
+    <dimen name="qs_brightness_margin_top">12dp</dimen>
     <dimen name="qs_brightness_margin_bottom">16dp</dimen>
     <dimen name="qqs_layout_margin_top">16dp</dimen>
     <dimen name="qqs_layout_padding_bottom">24dp</dimen>
@@ -572,6 +572,7 @@
     <dimen name="qs_header_row_min_height">48dp</dimen>
 
     <dimen name="qs_header_non_clickable_element_height">24dp</dimen>
+    <dimen name="new_qs_header_non_clickable_element_height">20dp</dimen>
 
     <dimen name="qs_footer_padding">20dp</dimen>
     <dimen name="qs_security_footer_height">88dp</dimen>
diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml
index 3164ed1..e30d441 100644
--- a/packages/SystemUI/res/values/integers.xml
+++ b/packages/SystemUI/res/values/integers.xml
@@ -28,4 +28,11 @@
 
     <!-- The time it takes for the over scroll release animation to complete, in milli seconds.  -->
     <integer name="lockscreen_shade_over_scroll_release_duration">0</integer>
+
+    <!-- Values for transition of QS Headers -->
+    <integer name="fade_out_complete_frame">14</integer>
+    <integer name="fade_in_start_frame">58</integer>
+    <!-- Percentage of displacement for items in QQS to guarantee matching with bottom of clock at
+         fade_out_complete_frame -->
+    <dimen name="percent_displacement_at_fade_out" format="float">0.1066</dimen>
 </resources>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index a734fa7..475ca91 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -128,11 +128,10 @@
     <!-- This is hard coded to be sans-serif-condensed to match the icons -->
 
     <style name="TextAppearance.QS.Status">
-        <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
         <item name="android:textColor">?android:attr/textColorPrimary</item>
         <item name="android:textSize">14sp</item>
         <item name="android:letterSpacing">0.01</item>
-        <item name="android:lineHeight">20sp</item>
     </style>
 
     <style name="TextAppearance.QS.SecurityFooter" parent="@style/TextAppearance.QS.Status">
@@ -143,12 +142,10 @@
     <style name="TextAppearance.QS.Status.Carriers" />
 
     <style name="TextAppearance.QS.Status.Carriers.NoCarrierText">
-        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
         <item name="android:textColor">?android:attr/textColorSecondary</item>
     </style>
 
     <style name="TextAppearance.QS.Status.Build">
-        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
         <item name="android:textColor">?android:attr/textColorSecondary</item>
     </style>
 
diff --git a/packages/SystemUI/res/xml/combined_qs_header_scene.xml b/packages/SystemUI/res/xml/combined_qs_header_scene.xml
index f3866c0..de855e2 100644
--- a/packages/SystemUI/res/xml/combined_qs_header_scene.xml
+++ b/packages/SystemUI/res/xml/combined_qs_header_scene.xml
@@ -27,67 +27,60 @@
             <KeyPosition
                 app:keyPositionType="deltaRelative"
                 app:percentX="0"
-                app:percentY="0"
-                app:framePosition="49"
+                app:percentY="@dimen/percent_displacement_at_fade_out"
+                app:framePosition="@integer/fade_out_complete_frame"
                 app:sizePercent="0"
                 app:curveFit="linear"
                 app:motionTarget="@id/date" />
             <KeyPosition
                 app:keyPositionType="deltaRelative"
                 app:percentX="1"
-                app:percentY="0.51"
+                app:percentY="0.5"
                 app:sizePercent="1"
-                app:framePosition="51"
+                app:framePosition="50"
                 app:curveFit="linear"
                 app:motionTarget="@id/date" />
             <KeyAttribute
                 app:motionTarget="@id/date"
-                app:framePosition="30"
+                app:framePosition="14"
                 android:alpha="0"
                 />
             <KeyAttribute
                 app:motionTarget="@id/date"
-                app:framePosition="70"
+                app:framePosition="@integer/fade_in_start_frame"
                 android:alpha="0"
                 />
             <KeyPosition
-                app:keyPositionType="pathRelative"
+                app:keyPositionType="deltaRelative"
                 app:percentX="0"
-                app:percentY="0"
-                app:framePosition="0"
-                app:curveFit="linear"
-                app:motionTarget="@id/statusIcons" />
-            <KeyPosition
-                app:keyPositionType="pathRelative"
-                app:percentX="0"
-                app:percentY="0"
-                app:framePosition="50"
+                app:percentY="@dimen/percent_displacement_at_fade_out"
+                app:framePosition="@integer/fade_out_complete_frame"
                 app:sizePercent="0"
                 app:curveFit="linear"
                 app:motionTarget="@id/statusIcons" />
             <KeyPosition
                 app:keyPositionType="deltaRelative"
                 app:percentX="1"
-                app:percentY="0.51"
-                app:framePosition="51"
+                app:percentY="0.5"
+                app:framePosition="50"
                 app:sizePercent="1"
                 app:curveFit="linear"
                 app:motionTarget="@id/statusIcons" />
             <KeyAttribute
                 app:motionTarget="@id/statusIcons"
-                app:framePosition="30"
+                app:framePosition="@integer/fade_out_complete_frame"
                 android:alpha="0"
                 />
             <KeyAttribute
                 app:motionTarget="@id/statusIcons"
-                app:framePosition="70"
+                app:framePosition="@integer/fade_in_start_frame"
                 android:alpha="0"
                 />
             <KeyPosition
                 app:keyPositionType="deltaRelative"
                 app:percentX="0"
-                app:percentY="0"
-                app:framePosition="50"
+                app:percentY="@dimen/percent_displacement_at_fade_out"
+                app:framePosition="@integer/fade_out_complete_frame"
                 app:percentWidth="1"
                 app:percentHeight="1"
                 app:curveFit="linear"
@@ -95,27 +88,27 @@
             <KeyPosition
                 app:keyPositionType="deltaRelative"
                 app:percentX="1"
-                app:percentY="0.51"
-                app:framePosition="51"
+                app:percentY="0.5"
+                app:framePosition="50"
                 app:percentWidth="1"
                 app:percentHeight="1"
                 app:curveFit="linear"
                 app:motionTarget="@id/batteryRemainingIcon" />
             <KeyAttribute
                 app:motionTarget="@id/batteryRemainingIcon"
-                app:framePosition="30"
+                app:framePosition="@integer/fade_out_complete_frame"
                 android:alpha="0"
                 />
             <KeyAttribute
                 app:motionTarget="@id/batteryRemainingIcon"
-                app:framePosition="70"
+                app:framePosition="@integer/fade_in_start_frame"
                 android:alpha="0"
                 />
             <KeyPosition
                 app:motionTarget="@id/carrier_group"
                 app:percentX="1"
-                app:percentY="0.51"
-                app:framePosition="51"
+                app:percentY="0.5"
+                app:framePosition="50"
                 app:percentWidth="1"
                 app:percentHeight="1"
                 app:curveFit="linear"
@@ -126,7 +119,7 @@
                 android:alpha="0" />
             <KeyAttribute
                 app:motionTarget="@id/carrier_group"
-                app:framePosition="70"
+                app:framePosition="@integer/fade_in_start_frame"
                 android:alpha="0" />
         </KeyFrameSet>
     </Transition>
diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml
index a82684d03..88b4f43 100644
--- a/packages/SystemUI/res/xml/qqs_header.xml
+++ b/packages/SystemUI/res/xml/qqs_header.xml
@@ -43,7 +43,8 @@
         android:id="@+id/date">
         <Layout
             android:layout_width="0dp"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
+            android:layout_marginStart="8dp"
             app:layout_constrainedWidth="true"
             app:layout_constraintStart_toEndOf="@id/clock"
             app:layout_constraintEnd_toStartOf="@id/barrier"
@@ -57,8 +58,8 @@
         android:id="@+id/statusIcons">
         <Layout
             android:layout_width="0dp"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
-            app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
+            app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintStart_toEndOf="@id/date"
             app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
             app:layout_constraintTop_toTopOf="parent"
@@ -71,9 +72,9 @@
         android:id="@+id/batteryRemainingIcon">
         <Layout
             android:layout_width="wrap_content"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constrainedWidth="true"
-            app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height"
+            app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintStart_toEndOf="@id/statusIcons"
             app:layout_constraintEnd_toEndOf="@id/end_guide"
             app:layout_constraintTop_toTopOf="parent"
diff --git a/packages/SystemUI/res/xml/qs_header_new.xml b/packages/SystemUI/res/xml/qs_header_new.xml
index f39e6bd..d8a4e77 100644
--- a/packages/SystemUI/res/xml/qs_header_new.xml
+++ b/packages/SystemUI/res/xml/qs_header_new.xml
@@ -40,13 +40,13 @@
             android:layout_height="@dimen/large_screen_shade_header_min_height"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toBottomOf="@id/privacy_container"
-            app:layout_constraintBottom_toTopOf="@id/date"
+            app:layout_constraintBottom_toBottomOf="@id/carrier_group"
             app:layout_constraintEnd_toStartOf="@id/carrier_group"
             app:layout_constraintHorizontal_bias="0"
         />
         <Transform
-            android:scaleX="2.4"
-            android:scaleY="2.4"
+            android:scaleX="2.57"
+            android:scaleY="2.57"
             />
     </Constraint>
 
@@ -54,11 +54,11 @@
         android:id="@+id/date">
         <Layout
             android:layout_width="0dp"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintEnd_toStartOf="@id/space"
             app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintTop_toBottomOf="@id/clock"
+            app:layout_constraintTop_toBottomOf="@id/carrier_group"
             app:layout_constraintHorizontal_bias="0"
             app:layout_constraintHorizontal_chainStyle="spread_inside"
         />
@@ -87,7 +87,7 @@
         android:id="@+id/statusIcons">
         <Layout
             android:layout_width="0dp"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constrainedWidth="true"
             app:layout_constraintStart_toEndOf="@id/space"
             app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
@@ -101,8 +101,8 @@
         android:id="@+id/batteryRemainingIcon">
         <Layout
             android:layout_width="wrap_content"
-            android:layout_height="@dimen/qs_header_non_clickable_element_height"
-            app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height"
+            android:layout_height="@dimen/new_qs_header_non_clickable_element_height"
+            app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height"
             app:layout_constraintStart_toEndOf="@id/statusIcons"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintTop_toTopOf="@id/date"
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 860a5da..134f3bc 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -20,16 +20,15 @@
 import android.annotation.FloatRange
 import android.annotation.IntRange
 import android.annotation.SuppressLint
-import android.app.compat.ChangeIdStateCache.invalidate
 import android.content.Context
 import android.graphics.Canvas
+import android.graphics.Rect
 import android.text.Layout
 import android.text.TextUtils
 import android.text.format.DateFormat
 import android.util.AttributeSet
+import android.util.MathUtils
 import android.widget.TextView
-import com.android.internal.R.attr.contentDescription
-import com.android.internal.R.attr.format
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.animation.GlyphCallback
 import com.android.systemui.animation.Interpolators
@@ -39,6 +38,8 @@
 import java.util.Calendar
 import java.util.Locale
 import java.util.TimeZone
+import kotlin.math.max
+import kotlin.math.min
 
 /**
  * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30)
@@ -311,7 +312,24 @@
         )
     }
 
-    private val glyphFilter: GlyphCallback? = null // Add text animation tweak here.
+    // The offset of each glyph from where it should be.
+    private var glyphOffsets = mutableListOf(0.0f, 0.0f, 0.0f, 0.0f)
+
+    private var lastSeenAnimationProgress = 1.0f
+
+    // If the animation is being reversed, the target offset for each glyph for the "stop".
+    private var animationCancelStartPosition = mutableListOf(0.0f, 0.0f, 0.0f, 0.0f)
+    private var animationCancelStopPosition = 0.0f
+
+    // Whether the currently playing animation needed a stop (and thus, is shortened).
+    private var currentAnimationNeededStop = false
+
+    private val glyphFilter: GlyphCallback = { positionedGlyph, _ ->
+        val offset = positionedGlyph.lineNo * DIGITS_PER_LINE + positionedGlyph.glyphIndex
+        if (offset < glyphOffsets.size) {
+            positionedGlyph.x += glyphOffsets[offset]
+        }
+    }
 
     /**
      * Set text style with an optional animation.
@@ -421,6 +439,124 @@
         pw.println("    time=$time")
     }
 
+    fun moveForSplitShade(fromRect: Rect, toRect: Rect, fraction: Float) {
+        // Do we need to cancel an in-flight animation?
+        // Need to also check against 0.0f here; we can sometimes get two calls with fraction == 0,
+        // which trips up the check otherwise.
+        if (lastSeenAnimationProgress != 1.0f &&
+                lastSeenAnimationProgress != 0.0f &&
+                fraction == 0.0f) {
+            // New animation, but need to stop the old one. Figure out where each glyph currently
+            // is in relation to the box position. After that, use the leading digit's current
+            // position as the stop target.
+            currentAnimationNeededStop = true
+
+            // We assume that the current glyph offsets would be relative to the "from" position.
+            val moveAmount = toRect.left - fromRect.left
+
+            // Remap the current glyph offsets to be relative to the new "end" position, and figure
+            // out the start/end positions for the stop animation.
+            for (i in 0 until NUM_DIGITS) {
+                glyphOffsets[i] = -moveAmount + glyphOffsets[i]
+                animationCancelStartPosition[i] = glyphOffsets[i]
+            }
+
+            // Use the leading digit's offset as the stop position.
+            if (toRect.left > fromRect.left) {
+                // It _was_ moving left
+                animationCancelStopPosition = glyphOffsets[0]
+            } else {
+                // It was moving right
+                animationCancelStopPosition = glyphOffsets[1]
+            }
+        }
+
+        // Is there a cancellation in progress?
+        if (currentAnimationNeededStop && fraction < ANIMATION_CANCELLATION_TIME) {
+            val animationStopProgress = MathUtils.constrainedMap(
+                    0.0f, 1.0f, 0.0f, ANIMATION_CANCELLATION_TIME, fraction
+            )
+
+            // One of the digits has already stopped.
+            val animationStopStep = 1.0f / (NUM_DIGITS - 1)
+
+            for (i in 0 until NUM_DIGITS) {
+                val stopAmount = if (toRect.left > fromRect.left) {
+                    // It was moving left (before flipping)
+                    MOVE_LEFT_DELAYS[i] * animationStopStep
+                } else {
+                    // It was moving right (before flipping)
+                    MOVE_RIGHT_DELAYS[i] * animationStopStep
+                }
+
+                // Leading digit stops immediately.
+                if (stopAmount == 0.0f) {
+                    glyphOffsets[i] = animationCancelStopPosition
+                } else {
+                    val actualStopAmount = MathUtils.constrainedMap(
+                            0.0f, 1.0f, 0.0f, stopAmount, animationStopProgress
+                    )
+                    val easedProgress = MOVE_INTERPOLATOR.getInterpolation(actualStopAmount)
+                    val glyphMoveAmount =
+                            animationCancelStopPosition - animationCancelStartPosition[i]
+                    glyphOffsets[i] =
+                            animationCancelStartPosition[i] + glyphMoveAmount * easedProgress
+                }
+            }
+        } else {
+            // Normal part of the animation.
+            // Do we need to remap the animation progress to take account of the cancellation?
+            val actualFraction = if (currentAnimationNeededStop) {
+                MathUtils.constrainedMap(
+                        0.0f, 1.0f, ANIMATION_CANCELLATION_TIME, 1.0f, fraction
+                )
+            } else {
+                fraction
+            }
+
+            val digitFractions = (0 until NUM_DIGITS).map {
+                // The delay for each digit, in terms of fraction (i.e. the digit should not move
+                // during 0.0 - 0.1).
+                val initialDelay = if (toRect.left > fromRect.left) {
+                    MOVE_RIGHT_DELAYS[it] * MOVE_DIGIT_STEP
+                } else {
+                    MOVE_LEFT_DELAYS[it] * MOVE_DIGIT_STEP
+                }
+
+                val f = MathUtils.constrainedMap(
+                        0.0f, 1.0f,
+                        initialDelay, initialDelay + AVAILABLE_ANIMATION_TIME,
+                        actualFraction
+                )
+                MOVE_INTERPOLATOR.getInterpolation(max(min(f, 1.0f), 0.0f))
+            }
+
+            // Was there an animation halt?
+            val moveAmount = if (currentAnimationNeededStop) {
+                // Only need to animate over the remaining space if the animation was aborted.
+                -animationCancelStopPosition
+            } else {
+                toRect.left.toFloat() - fromRect.left.toFloat()
+            }
+
+            for (i in 0 until NUM_DIGITS) {
+                glyphOffsets[i] = -moveAmount + (moveAmount * digitFractions[i])
+            }
+        }
+
+        invalidate()
+
+        if (fraction == 1.0f) {
+            // Reset
+            currentAnimationNeededStop = false
+        }
+
+        lastSeenAnimationProgress = fraction
+
+        // Ensure that the actual clock container is always in the "end" position.
+        this.setLeftTopRightBottom(toRect.left, toRect.top, toRect.right, toRect.bottom)
+    }
+
     // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often.
     // This is an optimization to ensure we only recompute the patterns when the inputs change.
     private object Patterns {
@@ -458,5 +594,36 @@
         private const val APPEAR_ANIM_DURATION: Long = 350
         private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500
         private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000
+
+        // Constants for the animation
+        private val MOVE_INTERPOLATOR = Interpolators.STANDARD
+
+        // Calculate the positions of all of the digits...
+        // Offset each digit by, say, 0.1
+        // This means that each digit needs to move over a slice of "fractions", i.e. digit 0 should
+        // move from 0.0 - 0.7, digit 1 from 0.1 - 0.8, digit 2 from 0.2 - 0.9, and digit 3
+        // from 0.3 - 1.0.
+        private const val NUM_DIGITS = 4
+        private const val DIGITS_PER_LINE = 2
+
+        // How much of "fraction" to spend on canceling the animation, if needed
+        private const val ANIMATION_CANCELLATION_TIME = 0.4f
+
+        // Delays. Each digit's animation should have a slight delay, so we get a nice
+        // "stepping" effect. When moving right, the second digit of the hour should move first.
+        // When moving left, the first digit of the hour should move first. The lists encode
+        // the delay for each digit (hour[0], hour[1], minute[0], minute[1]), to be multiplied
+        // by delayMultiplier.
+        private val MOVE_LEFT_DELAYS = listOf(0, 1, 2, 3)
+        private val MOVE_RIGHT_DELAYS = listOf(1, 0, 3, 2)
+
+        // How much delay to apply to each subsequent digit. This is measured in terms of "fraction"
+        // (i.e. a value of 0.1 would cause a digit to wait until fraction had hit 0.1, or 0.2 etc
+        // before moving).
+        private const val MOVE_DIGIT_STEP = 0.1f
+
+        // Total available transition time for each digit, taking into account the step. If step is
+        // 0.1, then digit 0 would animate over 0.0 - 0.7, making availableTime 0.7.
+        private val AVAILABLE_ANIMATION_TIME = 1.0f - MOVE_DIGIT_STEP * (NUM_DIGITS - 1)
     }
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index f03fee4..e3c21cc 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -22,6 +22,7 @@
 import android.provider.Settings
 import android.util.Log
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.internal.annotations.Keep
 import com.android.systemui.plugins.ClockController
 import com.android.systemui.plugins.ClockId
 import com.android.systemui.plugins.ClockMetadata
@@ -201,6 +202,7 @@
         val provider: ClockProvider
     )
 
+    @Keep
     private data class ClockSetting(
         val clockId: ClockId,
         val _applied_timestamp: Long?
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index b887951..6fd61da 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -16,6 +16,7 @@
 import android.content.Context
 import android.content.res.Resources
 import android.graphics.Color
+import android.graphics.Rect
 import android.icu.text.NumberFormat
 import android.util.TypedValue
 import android.view.LayoutInflater
@@ -130,6 +131,10 @@
             lp.topMargin = (-0.5f * view.bottom).toInt()
             view.setLayoutParams(lp)
         }
+
+        fun moveForSplitShade(fromRect: Rect, toRect: Rect, fraction: Float) {
+            view.moveForSplitShade(fromRect, toRect, fraction)
+        }
     }
 
     inner class DefaultClockEvents : ClockEvents {
@@ -209,6 +214,13 @@
                 clocks.forEach { it.animateDoze(dozeState.isActive, !hasJumped) }
             }
         }
+
+        override fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) {
+            largeClock.moveForSplitShade(fromRect, toRect, fraction)
+        }
+
+        override val hasCustomPositionUpdatedAnimation: Boolean
+            get() = true
     }
 
     private class AnimationState(
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
index dd2e55d..cd4b999 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
@@ -15,6 +15,7 @@
  */
 package com.android.systemui.shared.regionsampling
 
+import android.graphics.Color
 import android.graphics.Rect
 import android.view.View
 import androidx.annotation.VisibleForTesting
@@ -33,18 +34,19 @@
         regionSamplingEnabled: Boolean,
         updateFun: UpdateColorCallback
 ) {
-    private var isDark = RegionDarkness.DEFAULT
+    private var regionDarkness = RegionDarkness.DEFAULT
     private var samplingBounds = Rect()
     private val tmpScreenLocation = IntArray(2)
     @VisibleForTesting var regionSampler: RegionSamplingHelper? = null
-
+    private var lightForegroundColor = Color.WHITE
+    private var darkForegroundColor = Color.BLACK
     /**
      * Interface for method to be passed into RegionSamplingHelper
      */
     @FunctionalInterface
     interface UpdateColorCallback {
         /**
-         * Method to update the text colors after clock darkness changed.
+         * Method to update the foreground colors after clock darkness changed.
          */
         fun updateColors()
     }
@@ -59,6 +61,30 @@
         return RegionSamplingHelper(sampledView, callback, mainExecutor, bgExecutor)
     }
 
+    /**
+     * Sets the colors to be used for Dark and Light Foreground.
+     *
+     * @param lightColor The color used for Light Foreground.
+     * @param darkColor The color used for Dark Foreground.
+     */
+    fun setForegroundColors(lightColor: Int, darkColor: Int) {
+        lightForegroundColor = lightColor
+        darkForegroundColor = darkColor
+    }
+
+    /**
+     * Determines which foreground color to use based on region darkness.
+     *
+     * @return the determined foreground color
+     */
+    fun currentForegroundColor(): Int{
+        return if (regionDarkness.isDark) {
+            lightForegroundColor
+        } else {
+            darkForegroundColor
+        }
+    }
+
     private fun convertToClockDarkness(isRegionDark: Boolean): RegionDarkness {
         return if (isRegionDark) {
             RegionDarkness.DARK
@@ -68,7 +94,7 @@
     }
 
     fun currentRegionDarkness(): RegionDarkness {
-        return isDark
+        return regionDarkness
     }
 
     /**
@@ -97,7 +123,7 @@
             regionSampler = createRegionSamplingHelper(sampledView,
                     object : SamplingCallback {
                         override fun onRegionDarknessChanged(isRegionDark: Boolean) {
-                            isDark = convertToClockDarkness(isRegionDark)
+                            regionDarkness = convertToClockDarkness(isRegionDark)
                             updateFun.updateColors()
                         }
                         /**
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java
deleted file mode 100644
index 30c062b..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java
+++ /dev/null
@@ -1,380 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.shared.system;
-
-import android.graphics.HardwareRenderer;
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.os.Handler;
-import android.os.Handler.Callback;
-import android.os.Message;
-import android.os.Trace;
-import android.view.SurfaceControl;
-import android.view.SurfaceControl.Transaction;
-import android.view.View;
-import android.view.ViewRootImpl;
-
-import java.util.function.Consumer;
-
-/**
- * Helper class to apply surface transactions in sync with RenderThread.
- *
- * NOTE: This is a modification of {@link android.view.SyncRtSurfaceTransactionApplier}, we can't 
- *       currently reference that class from the shared lib as it is hidden.
- */
-public class SyncRtSurfaceTransactionApplierCompat {
-
-    public static final int FLAG_ALL = 0xffffffff;
-    public static final int FLAG_ALPHA = 1;
-    public static final int FLAG_MATRIX = 1 << 1;
-    public static final int FLAG_WINDOW_CROP = 1 << 2;
-    public static final int FLAG_LAYER = 1 << 3;
-    public static final int FLAG_CORNER_RADIUS = 1 << 4;
-    public static final int FLAG_BACKGROUND_BLUR_RADIUS = 1 << 5;
-    public static final int FLAG_VISIBILITY = 1 << 6;
-    public static final int FLAG_RELATIVE_LAYER = 1 << 7;
-    public static final int FLAG_SHADOW_RADIUS = 1 << 8;
-
-    private static final int MSG_UPDATE_SEQUENCE_NUMBER = 0;
-
-    private final SurfaceControl mBarrierSurfaceControl;
-    private final ViewRootImpl mTargetViewRootImpl;
-    private final Handler mApplyHandler;
-
-    private int mSequenceNumber = 0;
-    private int mPendingSequenceNumber = 0;
-    private Runnable mAfterApplyCallback;
-
-    /**
-     * @param targetView The view in the surface that acts as synchronization anchor.
-     */
-    public SyncRtSurfaceTransactionApplierCompat(View targetView) {
-        mTargetViewRootImpl = targetView != null ? targetView.getViewRootImpl() : null;
-        mBarrierSurfaceControl = mTargetViewRootImpl != null
-            ? mTargetViewRootImpl.getSurfaceControl() : null;
-
-        mApplyHandler = new Handler(new Callback() {
-            @Override
-            public boolean handleMessage(Message msg) {
-                if (msg.what == MSG_UPDATE_SEQUENCE_NUMBER) {
-                    onApplyMessage(msg.arg1);
-                    return true;
-                }
-                return false;
-            }
-        });
-    }
-
-    private void onApplyMessage(int seqNo) {
-        mSequenceNumber = seqNo;
-        if (mSequenceNumber == mPendingSequenceNumber && mAfterApplyCallback != null) {
-            Runnable r = mAfterApplyCallback;
-            mAfterApplyCallback = null;
-            r.run();
-        }
-    }
-
-    /**
-     * Schedules applying surface parameters on the next frame.
-     *
-     * @param params The surface parameters to apply. DO NOT MODIFY the list after passing into
-     *               this method to avoid synchronization issues.
-     */
-    public void scheduleApply(final SyncRtSurfaceTransactionApplierCompat.SurfaceParams... params) {
-        if (mTargetViewRootImpl == null || mTargetViewRootImpl.getView() == null) {
-            return;
-        }
-
-        mPendingSequenceNumber++;
-        final int toApplySeqNo = mPendingSequenceNumber;
-        mTargetViewRootImpl.registerRtFrameCallback(new HardwareRenderer.FrameDrawingCallback() {
-            @Override
-            public void onFrameDraw(long frame) {
-                if (mBarrierSurfaceControl == null || !mBarrierSurfaceControl.isValid()) {
-                    Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
-                            .sendToTarget();
-                    return;
-                }
-                Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Sync transaction frameNumber=" + frame);
-                Transaction t = new Transaction();
-                for (int i = params.length - 1; i >= 0; i--) {
-                    SyncRtSurfaceTransactionApplierCompat.SurfaceParams surfaceParams =
-                            params[i];
-                    surfaceParams.applyTo(t);
-                }
-                if (mTargetViewRootImpl != null) {
-                    mTargetViewRootImpl.mergeWithNextTransaction(t, frame);
-                } else {
-                    t.apply();
-                }
-                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
-                Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
-                        .sendToTarget();
-            }
-        });
-
-        // Make sure a frame gets scheduled.
-        mTargetViewRootImpl.getView().invalidate();
-    }
-
-    /**
-     * Calls the runnable when any pending apply calls have completed
-     */
-    public void addAfterApplyCallback(final Runnable afterApplyCallback) {
-        if (mSequenceNumber == mPendingSequenceNumber) {
-            afterApplyCallback.run();
-        } else {
-            if (mAfterApplyCallback == null) {
-                mAfterApplyCallback = afterApplyCallback;
-            } else {
-                final Runnable oldCallback = mAfterApplyCallback;
-                mAfterApplyCallback = new Runnable() {
-                    @Override
-                    public void run() {
-                        afterApplyCallback.run();
-                        oldCallback.run();
-                    }
-                };
-            }
-        }
-    }
-
-    public static void applyParams(TransactionCompat t,
-            SyncRtSurfaceTransactionApplierCompat.SurfaceParams params) {
-        params.applyTo(t.mTransaction);
-    }
-
-    /**
-     * Creates an instance of SyncRtSurfaceTransactionApplier, deferring until the target view is
-     * attached if necessary.
-     */
-    public static void create(final View targetView,
-            final Consumer<SyncRtSurfaceTransactionApplierCompat> callback) {
-        if (targetView == null) {
-            // No target view, no applier
-            callback.accept(null);
-        } else if (targetView.getViewRootImpl() != null) {
-            // Already attached, we're good to go
-            callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView));
-        } else {
-            // Haven't been attached before we can get the view root
-            targetView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
-                @Override
-                public void onViewAttachedToWindow(View v) {
-                    targetView.removeOnAttachStateChangeListener(this);
-                    callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView));
-                }
-
-                @Override
-                public void onViewDetachedFromWindow(View v) {
-                    // Do nothing
-                }
-            });
-        }
-    }
-
-    public static class SurfaceParams {
-        public static class Builder {
-            final SurfaceControl surface;
-            int flags;
-            float alpha;
-            float cornerRadius;
-            int backgroundBlurRadius;
-            Matrix matrix;
-            Rect windowCrop;
-            int layer;
-            SurfaceControl relativeTo;
-            int relativeLayer;
-            boolean visible;
-            float shadowRadius;
-
-            /**
-             * @param surface The surface to modify.
-             */
-            public Builder(SurfaceControl surface) {
-                this.surface = surface;
-            }
-
-            /**
-             * @param alpha The alpha value to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withAlpha(float alpha) {
-                this.alpha = alpha;
-                flags |= FLAG_ALPHA;
-                return this;
-            }
-
-            /**
-             * @param matrix The matrix to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withMatrix(Matrix matrix) {
-                this.matrix = new Matrix(matrix);
-                flags |= FLAG_MATRIX;
-                return this;
-            }
-
-            /**
-             * @param windowCrop The window crop to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withWindowCrop(Rect windowCrop) {
-                this.windowCrop = new Rect(windowCrop);
-                flags |= FLAG_WINDOW_CROP;
-                return this;
-            }
-
-            /**
-             * @param layer The layer to assign the surface.
-             * @return this Builder
-             */
-            public Builder withLayer(int layer) {
-                this.layer = layer;
-                flags |= FLAG_LAYER;
-                return this;
-            }
-
-            /**
-             * @param relativeTo The surface that's set relative layer to.
-             * @param relativeLayer The relative layer.
-             * @return this Builder
-             */
-            public Builder withRelativeLayerTo(SurfaceControl relativeTo, int relativeLayer) {
-                this.relativeTo = relativeTo;
-                this.relativeLayer = relativeLayer;
-                flags |= FLAG_RELATIVE_LAYER;
-                return this;
-            }
-
-            /**
-             * @param radius the Radius for rounded corners to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withCornerRadius(float radius) {
-                this.cornerRadius = radius;
-                flags |= FLAG_CORNER_RADIUS;
-                return this;
-            }
-
-            /**
-             * @param radius the Radius for the shadows to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withShadowRadius(float radius) {
-                this.shadowRadius = radius;
-                flags |= FLAG_SHADOW_RADIUS;
-                return this;
-            }
-
-            /**
-             * @param radius the Radius for blur to apply to the background surfaces.
-             * @return this Builder
-             */
-            public Builder withBackgroundBlur(int radius) {
-                this.backgroundBlurRadius = radius;
-                flags |= FLAG_BACKGROUND_BLUR_RADIUS;
-                return this;
-            }
-
-            /**
-             * @param visible The visibility to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withVisibility(boolean visible) {
-                this.visible = visible;
-                flags |= FLAG_VISIBILITY;
-                return this;
-            }
-
-            /**
-             * @return a new SurfaceParams instance
-             */
-            public SurfaceParams build() {
-                return new SurfaceParams(surface, flags, alpha, matrix, windowCrop, layer,
-                        relativeTo, relativeLayer, cornerRadius, backgroundBlurRadius, visible,
-                        shadowRadius);
-            }
-        }
-
-        private SurfaceParams(SurfaceControl surface, int flags, float alpha, Matrix matrix,
-                Rect windowCrop, int layer, SurfaceControl relativeTo, int relativeLayer,
-                float cornerRadius, int backgroundBlurRadius, boolean visible, float shadowRadius) {
-            this.flags = flags;
-            this.surface = surface;
-            this.alpha = alpha;
-            this.matrix = matrix;
-            this.windowCrop = windowCrop;
-            this.layer = layer;
-            this.relativeTo = relativeTo;
-            this.relativeLayer = relativeLayer;
-            this.cornerRadius = cornerRadius;
-            this.backgroundBlurRadius = backgroundBlurRadius;
-            this.visible = visible;
-            this.shadowRadius = shadowRadius;
-        }
-
-        private final int flags;
-        private final float[] mTmpValues = new float[9];
-
-        public final SurfaceControl surface;
-        public final float alpha;
-        public final float cornerRadius;
-        public final int backgroundBlurRadius;
-        public final Matrix matrix;
-        public final Rect windowCrop;
-        public final int layer;
-        public final SurfaceControl relativeTo;
-        public final int relativeLayer;
-        public final boolean visible;
-        public final float shadowRadius;
-
-        public void applyTo(SurfaceControl.Transaction t) {
-            if ((flags & FLAG_MATRIX) != 0) {
-                t.setMatrix(surface, matrix, mTmpValues);
-            }
-            if ((flags & FLAG_WINDOW_CROP) != 0) {
-                t.setWindowCrop(surface, windowCrop);
-            }
-            if ((flags & FLAG_ALPHA) != 0) {
-                t.setAlpha(surface, alpha);
-            }
-            if ((flags & FLAG_LAYER) != 0) {
-                t.setLayer(surface, layer);
-            }
-            if ((flags & FLAG_CORNER_RADIUS) != 0) {
-                t.setCornerRadius(surface, cornerRadius);
-            }
-            if ((flags & FLAG_BACKGROUND_BLUR_RADIUS) != 0) {
-                t.setBackgroundBlurRadius(surface, backgroundBlurRadius);
-            }
-            if ((flags & FLAG_VISIBILITY) != 0) {
-                if (visible) {
-                    t.show(surface);
-                } else {
-                    t.hide(surface);
-                }
-            }
-            if ((flags & FLAG_RELATIVE_LAYER) != 0) {
-                t.setRelativeLayer(surface, relativeTo, relativeLayer);
-            }
-            if ((flags & FLAG_SHADOW_RADIUS) != 0) {
-                t.setShadowRadius(surface, shadowRadius);
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java
deleted file mode 100644
index 43a882a5..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.shared.system;
-
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.view.SurfaceControl;
-import android.view.SurfaceControl.Transaction;
-
-public class TransactionCompat {
-
-    final Transaction mTransaction;
-
-    final float[] mTmpValues = new float[9];
-
-    public TransactionCompat() {
-        mTransaction = new Transaction();
-    }
-
-    public void apply() {
-        mTransaction.apply();
-    }
-
-    public TransactionCompat show(SurfaceControl surfaceControl) {
-        mTransaction.show(surfaceControl);
-        return this;
-    }
-
-    public TransactionCompat hide(SurfaceControl surfaceControl) {
-        mTransaction.hide(surfaceControl);
-        return this;
-    }
-
-    public TransactionCompat setPosition(SurfaceControl surfaceControl, float x, float y) {
-        mTransaction.setPosition(surfaceControl, x, y);
-        return this;
-    }
-
-    public TransactionCompat setSize(SurfaceControl surfaceControl, int w, int h) {
-        mTransaction.setBufferSize(surfaceControl, w, h);
-        return this;
-    }
-
-    public TransactionCompat setLayer(SurfaceControl surfaceControl, int z) {
-        mTransaction.setLayer(surfaceControl, z);
-        return this;
-    }
-
-    public TransactionCompat setAlpha(SurfaceControl surfaceControl, float alpha) {
-        mTransaction.setAlpha(surfaceControl, alpha);
-        return this;
-    }
-
-    public TransactionCompat setOpaque(SurfaceControl surfaceControl, boolean opaque) {
-        mTransaction.setOpaque(surfaceControl, opaque);
-        return this;
-    }
-
-    public TransactionCompat setMatrix(SurfaceControl surfaceControl, float dsdx, float dtdx,
-            float dtdy, float dsdy) {
-        mTransaction.setMatrix(surfaceControl, dsdx, dtdx, dtdy, dsdy);
-        return this;
-    }
-
-    public TransactionCompat setMatrix(SurfaceControl surfaceControl, Matrix matrix) {
-        mTransaction.setMatrix(surfaceControl, matrix, mTmpValues);
-        return this;
-    }
-
-    public TransactionCompat setWindowCrop(SurfaceControl surfaceControl, Rect crop) {
-        mTransaction.setWindowCrop(surfaceControl, crop);
-        return this;
-    }
-
-    public TransactionCompat setCornerRadius(SurfaceControl surfaceControl, float radius) {
-        mTransaction.setCornerRadius(surfaceControl, radius);
-        return this;
-    }
-
-    public TransactionCompat setBackgroundBlurRadius(SurfaceControl surfaceControl, int radius) {
-        mTransaction.setBackgroundBlurRadius(surfaceControl, radius);
-        return this;
-    }
-
-    public TransactionCompat setColor(SurfaceControl surfaceControl, float[] color) {
-        mTransaction.setColor(surfaceControl, color);
-        return this;
-    }
-
-    public static void setRelativeLayer(Transaction t, SurfaceControl surfaceControl,
-            SurfaceControl relativeTo, int z) {
-        t.setRelativeLayer(surfaceControl, relativeTo, z);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
index 0075ddd..5277e40 100644
--- a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
+++ b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
@@ -16,19 +16,29 @@
 
 package com.android.keyguard
 
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
 import android.content.Context
 import android.content.res.ColorStateList
 import android.content.res.TypedArray
 import android.graphics.Color
 import android.util.AttributeSet
+import android.view.View
 import com.android.settingslib.Utils
+import com.android.systemui.animation.Interpolators
 
 /** Displays security messages for the keyguard bouncer. */
-class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) :
+open class BouncerKeyguardMessageArea(context: Context?, attrs: AttributeSet?) :
     KeyguardMessageArea(context, attrs) {
     private val DEFAULT_COLOR = -1
     private var mDefaultColorState: ColorStateList? = null
     private var mNextMessageColorState: ColorStateList? = ColorStateList.valueOf(DEFAULT_COLOR)
+    private val animatorSet = AnimatorSet()
+    private var textAboutToShow: CharSequence? = null
+    protected open val SHOW_DURATION_MILLIS = 150L
+    protected open val HIDE_DURATION_MILLIS = 200L
 
     override fun updateTextColor() {
         var colorState = mDefaultColorState
@@ -58,4 +68,46 @@
         mDefaultColorState = Utils.getColorAttr(context, android.R.attr.textColorPrimary)
         super.reloadColor()
     }
+
+    override fun setMessage(msg: CharSequence?) {
+        if (msg == textAboutToShow || msg == text) {
+            return
+        }
+        textAboutToShow = msg
+
+        if (animatorSet.isRunning) {
+            animatorSet.cancel()
+            textAboutToShow = null
+        }
+
+        val hideAnimator =
+            ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f).apply {
+                duration = HIDE_DURATION_MILLIS
+                interpolator = Interpolators.STANDARD_ACCELERATE
+            }
+
+        hideAnimator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator?) {
+                    super@BouncerKeyguardMessageArea.setMessage(msg)
+                }
+            }
+        )
+        val showAnimator =
+            ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f).apply {
+                duration = SHOW_DURATION_MILLIS
+                interpolator = Interpolators.STANDARD_DECELERATE
+            }
+
+        showAnimator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator?) {
+                    textAboutToShow = null
+                }
+            }
+        )
+
+        animatorSet.playSequentially(hideAnimator, showAnimator)
+        animatorSet.start()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index b450ec3..20d064b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -40,6 +40,7 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.plugins.ClockController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.clocks.ClockRegistry;
@@ -404,5 +405,9 @@
             clock.dump(pw);
         }
     }
-}
 
+    /** Gets the animations for the current clock. */
+    public ClockAnimations getClockAnimations() {
+        return getClock().getAnimations();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index f26b905..73229c3 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -152,6 +152,7 @@
     }
 
     public void startAppearAnimation() {
+        mMessageAreaController.setMessage(getInitialMessageResId());
         mView.startAppearAnimation();
     }
 
@@ -169,6 +170,11 @@
         return view.indexOfChild(mView);
     }
 
+    /** Determines the message to show in the bouncer when it first appears. */
+    protected int getInitialMessageResId() {
+        return 0;
+    }
+
     /** Factory for a {@link KeyguardInputViewController}. */
     public static class Factory {
         private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
index c2802f7..2bd3ca5 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java
@@ -18,7 +18,6 @@
 
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
-import android.text.TextUtils;
 
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -100,15 +99,6 @@
         mView.setMessage(resId);
     }
 
-    /**
-     * Set Text if KeyguardMessageArea is empty.
-     */
-    public void setMessageIfEmpty(int resId) {
-        if (TextUtils.isEmpty(mView.getText())) {
-            setMessage(resId);
-        }
-    }
-
     public void setNextMessageColor(ColorStateList colorState) {
         mView.setNextMessageColor(colorState);
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
index 29e912f..0025986 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
@@ -187,7 +187,7 @@
     @Override
     void resetState() {
         mPasswordEntry.setTextOperationUser(UserHandle.of(KeyguardUpdateMonitor.getCurrentUser()));
-        mMessageAreaController.setMessage("");
+        mMessageAreaController.setMessage(getInitialMessageResId());
         final boolean wasDisabled = mPasswordEntry.isEnabled();
         mView.setPasswordEntryEnabled(true);
         mView.setPasswordEntryInputEnabled(true);
@@ -207,7 +207,6 @@
         if (reason != KeyguardSecurityView.SCREEN_ON || mShowImeAtScreenOn) {
             showInput();
         }
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_password);
     }
 
     private void showInput() {
@@ -324,4 +323,9 @@
                 //enabled input method subtype (The current IME should be LatinIME.)
                 || imm.getEnabledInputMethodSubtypeList(null, false).size() > 1;
     }
+
+    @Override
+    protected int getInitialMessageResId() {
+        return R.string.keyguard_enter_your_password;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index 9871645..1f0bd54 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -298,12 +298,6 @@
     }
 
     @Override
-    public void onResume(int reason) {
-        super.onResume(reason);
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pattern);
-    }
-
-    @Override
     public boolean needsInput() {
         return false;
     }
@@ -361,7 +355,7 @@
     }
 
     private void displayDefaultSecurityMessage() {
-        mMessageAreaController.setMessage("");
+        mMessageAreaController.setMessage(getInitialMessageResId());
     }
 
     private void handleAttemptLockout(long elapsedRealtimeDeadline) {
@@ -392,4 +386,9 @@
 
         }.start();
     }
+
+    @Override
+    protected int getInitialMessageResId() {
+        return R.string.keyguard_enter_your_pattern;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
index 59a018a..f7423ed 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -127,7 +127,6 @@
     public void onResume(int reason) {
         super.onResume(reason);
         mPasswordEntry.requestFocus();
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
index 89fcc47..7876f07 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
@@ -76,20 +76,13 @@
     }
 
     @Override
-    void resetState() {
-        super.resetState();
-        mMessageAreaController.setMessage("");
-    }
-
-    @Override
-    public void startAppearAnimation() {
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin);
-        super.startAppearAnimation();
-    }
-
-    @Override
     public boolean startDisappearAnimation(Runnable finishRunnable) {
         return mView.startDisappearAnimation(
                 mKeyguardUpdateMonitor.needsSlowUnlockTransition(), finishRunnable);
     }
+
+    @Override
+    protected int getInitialMessageResId() {
+        return R.string.keyguard_enter_your_pin;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index e9f06ed..7849747 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -20,6 +20,7 @@
 import android.util.Slog;
 
 import com.android.keyguard.KeyguardClockSwitch.ClockSize;
+import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
@@ -232,4 +233,9 @@
             mView.setClipBounds(null);
         }
     }
+
+    /** Gets the animations for the current clock. */
+    public ClockAnimations getClockAnimations() {
+        return mKeyguardClockSwitchController.getClockAnimations();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
index 2c2ab7b..6264ce7 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/BiometricMessageDeferralLogger.kt
@@ -17,9 +17,9 @@
 package com.android.keyguard.logging
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
 import com.android.systemui.log.dagger.BiometricMessagesLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
 import javax.inject.Inject
 
 /** Helper class for logging for [com.android.systemui.biometrics.FaceHelpMessageDeferral] */
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
index 50012a5..46f3d4e 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
@@ -16,15 +16,15 @@
 
 package com.android.keyguard.logging
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.VERBOSE
-import com.android.systemui.log.LogLevel.WARNING
-import com.android.systemui.log.MessageInitializer
-import com.android.systemui.log.MessagePrinter
 import com.android.systemui.log.dagger.KeyguardLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
+import com.android.systemui.plugins.log.LogLevel.WARNING
+import com.android.systemui.plugins.log.MessageInitializer
+import com.android.systemui.plugins.log.MessagePrinter
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 2eee957..82b32cf 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -22,13 +22,13 @@
 import com.android.keyguard.ActiveUnlockConfig
 import com.android.keyguard.KeyguardListenModel
 import com.android.keyguard.KeyguardUpdateMonitorCallback
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.VERBOSE
-import com.android.systemui.log.LogLevel.WARNING
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
+import com.android.systemui.plugins.log.LogLevel.WARNING
 import com.android.systemui.log.dagger.KeyguardUpdateMonitorLog
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 242a598..9493975 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -792,7 +792,11 @@
             mUdfpsBounds = udfpsProp.getLocation().getRect();
             mUdfpsBounds.scale(mScaleFactor);
             mUdfpsController.updateOverlayParams(udfpsProp.sensorId,
-                    new UdfpsOverlayParams(mUdfpsBounds, mCachedDisplayInfo.getNaturalWidth(),
+                    new UdfpsOverlayParams(mUdfpsBounds, new Rect(
+                            0, mCachedDisplayInfo.getNaturalHeight() / 2,
+                            mCachedDisplayInfo.getNaturalWidth(),
+                            mCachedDisplayInfo.getNaturalHeight()),
+                            mCachedDisplayInfo.getNaturalWidth(),
                             mCachedDisplayInfo.getNaturalHeight(), mScaleFactor,
                             mCachedDisplayInfo.rotation));
             if (!Objects.equals(previousUdfpsBounds, mUdfpsBounds)) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt
index 39199d1..0d08b43 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsLogger.kt
@@ -16,12 +16,12 @@
 
 package com.android.systemui.biometrics
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.VERBOSE
-import com.android.systemui.log.LogLevel.WARNING
 import com.android.systemui.log.dagger.UdfpsLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
+import com.android.systemui.plugins.log.LogLevel.WARNING
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
new file mode 100644
index 0000000..6e78f3d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.PixelFormat
+import android.graphics.Rect
+import android.hardware.fingerprint.FingerprintManager
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback
+import android.os.Handler
+import android.view.MotionEvent
+import android.view.View
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.concurrency.Execution
+import java.util.*
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+private const val TAG = "UdfpsOverlay"
+
+@SuppressLint("ClickableViewAccessibility")
+@SysUISingleton
+class UdfpsOverlay
+@Inject
+constructor(
+    private val context: Context,
+    private val execution: Execution,
+    private val windowManager: WindowManager,
+    private val fingerprintManager: FingerprintManager?,
+    private val handler: Handler,
+    private val biometricExecutor: Executor,
+    private val alternateTouchProvider: Optional<AlternateUdfpsTouchProvider>,
+    private val fgExecutor: DelayableExecutor,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val authController: AuthController,
+    private val udfpsLogger: UdfpsLogger
+) : CoreStartable {
+
+    /** The view, when [isShowing], or null. */
+    var overlayView: UdfpsOverlayView? = null
+        private set
+
+    private var requestId: Long = 0
+    private var onFingerDown = false
+    val size = windowManager.maximumWindowMetrics.bounds
+    val udfpsProps: MutableList<FingerprintSensorPropertiesInternal> = mutableListOf()
+
+    private var params: UdfpsOverlayParams = UdfpsOverlayParams()
+
+    private val coreLayoutParams =
+        WindowManager.LayoutParams(
+                WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG,
+                0 /* flags set in computeLayoutParams() */,
+                PixelFormat.TRANSLUCENT
+            )
+            .apply {
+                title = TAG
+                fitInsetsTypes = 0
+                gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT
+                layoutInDisplayCutoutMode =
+                    WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+                flags = Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS
+                privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+                // Avoid announcing window title.
+                accessibilityTitle = " "
+                inputFeatures = INPUT_FEATURE_SPY
+            }
+
+    fun onTouch(v: View, event: MotionEvent): Boolean {
+        val view = v as UdfpsOverlayView
+
+        return when (event.action) {
+            MotionEvent.ACTION_DOWN,
+            MotionEvent.ACTION_MOVE -> {
+                onFingerDown = true
+                if (!view.isDisplayConfigured && alternateTouchProvider.isPresent) {
+                    biometricExecutor.execute {
+                        alternateTouchProvider
+                            .get()
+                            .onPointerDown(
+                                requestId,
+                                event.x.toInt(),
+                                event.y.toInt(),
+                                event.touchMinor,
+                                event.touchMajor
+                            )
+                    }
+                    fgExecutor.execute {
+                        if (keyguardUpdateMonitor.isFingerprintDetectionRunning) {
+                            keyguardUpdateMonitor.onUdfpsPointerDown(requestId.toInt())
+                        }
+                    }
+
+                    view.configureDisplay {
+                        biometricExecutor.execute { alternateTouchProvider.get().onUiReady() }
+                    }
+                }
+
+                true
+            }
+            MotionEvent.ACTION_UP,
+            MotionEvent.ACTION_CANCEL -> {
+                if (onFingerDown && alternateTouchProvider.isPresent) {
+                    biometricExecutor.execute {
+                        alternateTouchProvider.get().onPointerUp(requestId)
+                    }
+                    fgExecutor.execute {
+                        if (keyguardUpdateMonitor.isFingerprintDetectionRunning) {
+                            keyguardUpdateMonitor.onUdfpsPointerUp(requestId.toInt())
+                        }
+                    }
+                }
+                onFingerDown = false
+                if (view.isDisplayConfigured) {
+                    view.unconfigureDisplay()
+                }
+
+                true
+            }
+            else -> false
+        }
+    }
+
+    fun show(requestId: Long): Boolean {
+        this.requestId = requestId
+        if (overlayView == null && alternateTouchProvider.isPresent) {
+            UdfpsOverlayView(context, null).let {
+                it.overlayParams = params
+                it.setUdfpsDisplayMode(
+                    UdfpsDisplayMode(context, execution, authController, udfpsLogger)
+                )
+                it.setOnTouchListener { v, event -> onTouch(v, event) }
+                overlayView = it
+            }
+            windowManager.addView(overlayView, coreLayoutParams)
+            return true
+        }
+
+        return false
+    }
+
+    fun hide() {
+        overlayView?.apply {
+            windowManager.removeView(this)
+            setOnTouchListener(null)
+        }
+
+        overlayView = null
+    }
+
+    @Override
+    override fun start() {
+        fingerprintManager?.addAuthenticatorsRegisteredCallback(
+            object : IFingerprintAuthenticatorsRegisteredCallback.Stub() {
+                override fun onAllAuthenticatorsRegistered(
+                    sensors: List<FingerprintSensorPropertiesInternal>
+                ) {
+                    handler.post { handleAllFingerprintAuthenticatorsRegistered(sensors) }
+                }
+            }
+        )
+    }
+
+    private fun handleAllFingerprintAuthenticatorsRegistered(
+        sensors: List<FingerprintSensorPropertiesInternal>
+    ) {
+        for (props in sensors) {
+            if (props.isAnyUdfpsType) {
+                udfpsProps.add(props)
+            }
+        }
+
+        // Setup param size
+        if (udfpsProps.isNotEmpty()) {
+            params =
+                UdfpsOverlayParams(
+                    sensorBounds = udfpsProps[0].location.rect,
+                    overlayBounds = Rect(0, size.height() / 2, size.width(), size.height()),
+                    naturalDisplayWidth = size.width(),
+                    naturalDisplayHeight = size.height(),
+                    scaleFactor = 1f
+                )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt
index d725dfb..c23b0f0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt
@@ -20,6 +20,7 @@
 
 data class UdfpsOverlayParams(
     val sensorBounds: Rect = Rect(),
+    val overlayBounds: Rect = Rect(),
     val naturalDisplayWidth: Int = 0,
     val naturalDisplayHeight: Int = 0,
     val scaleFactor: Float = 1f,
@@ -40,4 +41,4 @@
         } else {
             naturalDisplayHeight
         }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt
new file mode 100644
index 0000000..d371332
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.RectF
+import android.util.AttributeSet
+import android.widget.FrameLayout
+
+private const val TAG = "UdfpsOverlayView"
+
+class UdfpsOverlayView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
+
+    private val sensorRect = RectF()
+    var overlayParams = UdfpsOverlayParams()
+    private var mUdfpsDisplayMode: UdfpsDisplayMode? = null
+
+    var overlayPaint = Paint()
+    var sensorPaint = Paint()
+    val centerPaint = Paint()
+
+    /** True after the call to [configureDisplay] and before the call to [unconfigureDisplay]. */
+    var isDisplayConfigured: Boolean = false
+        private set
+
+    init {
+        this.setWillNotDraw(false)
+    }
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+
+        overlayPaint.color = Color.argb(120, 255, 0, 0)
+        overlayPaint.style = Paint.Style.FILL
+
+        sensorPaint.color = Color.argb(150, 134, 204, 255)
+        sensorPaint.style = Paint.Style.FILL
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        super.onDraw(canvas)
+
+        canvas.drawRect(overlayParams.overlayBounds, overlayPaint)
+        canvas.drawRect(overlayParams.sensorBounds, sensorPaint)
+        canvas.drawCircle(
+            overlayParams.sensorBounds.exactCenterX(),
+            overlayParams.sensorBounds.exactCenterY(),
+            overlayParams.sensorBounds.width().toFloat() / 2,
+            centerPaint
+        )
+    }
+
+    fun setUdfpsDisplayMode(udfpsDisplayMode: UdfpsDisplayMode?) {
+        mUdfpsDisplayMode = udfpsDisplayMode
+    }
+
+    fun configureDisplay(onDisplayConfigured: Runnable) {
+        isDisplayConfigured = true
+        mUdfpsDisplayMode?.enable(onDisplayConfigured)
+    }
+
+    fun unconfigureDisplay() {
+        isDisplayConfigured = false
+        mUdfpsDisplayMode?.disable(null /* onDisabled */)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt
index b1d6e00..75640b7 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.biometrics
 
+import android.content.Context
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER
@@ -23,9 +24,9 @@
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_UNKNOWN
-import android.hardware.fingerprint.IUdfpsOverlayController
 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
 import android.util.Log
+import android.view.LayoutInflater
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.commandline.Command
 import com.android.systemui.statusbar.commandline.CommandRegistry
@@ -41,14 +42,17 @@
  */
 @SysUISingleton
 class UdfpsShell @Inject constructor(
-    commandRegistry: CommandRegistry
+    commandRegistry: CommandRegistry,
+    private val udfpsOverlay: UdfpsOverlay
 ) : Command {
 
     /**
      * Set in [UdfpsController.java] constructor, used to show and hide the UDFPS overlay.
      * TODO: inject after b/229290039 is resolved
      */
-    var udfpsOverlayController: IUdfpsOverlayController? = null
+    var udfpsOverlayController: UdfpsController.UdfpsOverlayController? = null
+    var context: Context? = null
+    var inflater: LayoutInflater? = null
 
     init {
         commandRegistry.registerCommand("udfps") { this }
@@ -57,6 +61,11 @@
     override fun execute(pw: PrintWriter, args: List<String>) {
         if (args.size == 1 && args[0] == "hide") {
             hideOverlay()
+        } else if (args.size == 2 && args[0] == "udfpsOverlay" && args[1] == "show") {
+            hideOverlay()
+            showUdfpsOverlay()
+        } else if (args.size == 2 && args[0] == "udfpsOverlay" && args[1] == "hide") {
+            hideUdfpsOverlay()
         } else if (args.size == 2 && args[0] == "show") {
             showOverlay(getEnrollmentReason(args[1]))
         } else {
@@ -104,7 +113,17 @@
         )
     }
 
+    private fun showUdfpsOverlay() {
+        Log.v(TAG, "showUdfpsOverlay")
+        udfpsOverlay.show(REQUEST_ID)
+    }
+
+    private fun hideUdfpsOverlay() {
+        Log.v(TAG, "hideUdfpsOverlay")
+        udfpsOverlay.hide()
+    }
+
     private fun hideOverlay() {
         udfpsOverlayController?.hideUdfpsOverlay(SENSOR_ID)
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt
index 96af42b..d99625a 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BluetoothLogger.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.bluetooth
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.BluetoothLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 /** Helper class for logging bluetooth events. */
diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt b/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt
index 5b3a982..d27708f 100644
--- a/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/broadcast/logging/BroadcastDispatcherLogger.kt
@@ -20,11 +20,11 @@
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogMessage
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogMessage
 import com.android.systemui.log.dagger.BroadcastDispatcherLog
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
index 77b6523..d3b5d0e 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt
@@ -21,6 +21,8 @@
 import android.content.Intent
 import android.content.IntentFilter
 import android.os.Bundle
+import android.os.RemoteException
+import android.service.dreams.IDreamManager
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowInsets
@@ -40,11 +42,13 @@
  */
 class ControlsActivity @Inject constructor(
     private val uiController: ControlsUiController,
-    private val broadcastDispatcher: BroadcastDispatcher
+    private val broadcastDispatcher: BroadcastDispatcher,
+    private val dreamManager: IDreamManager,
 ) : ComponentActivity() {
 
     private lateinit var parent: ViewGroup
     private lateinit var broadcastReceiver: BroadcastReceiver
+    private var mExitToDream: Boolean = false
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -81,17 +85,36 @@
 
         parent = requireViewById<ViewGroup>(R.id.global_actions_controls)
         parent.alpha = 0f
-        uiController.show(parent, { finish() }, this)
+        uiController.show(parent, { finishOrReturnToDream() }, this)
 
         ControlsAnimations.enterAnimation(parent).start()
     }
 
-    override fun onBackPressed() {
+    override fun onResume() {
+        super.onResume()
+        mExitToDream = intent.getBooleanExtra(ControlsUiController.EXIT_TO_DREAM, false)
+    }
+
+    fun finishOrReturnToDream() {
+        if (mExitToDream) {
+            try {
+                mExitToDream = false
+                dreamManager.dream()
+                return
+            } catch (e: RemoteException) {
+                // Fall through
+            }
+        }
         finish()
     }
 
+    override fun onBackPressed() {
+        finishOrReturnToDream()
+    }
+
     override fun onStop() {
         super.onStop()
+        mExitToDream = false
 
         uiController.hide()
     }
@@ -106,7 +129,8 @@
         broadcastReceiver = object : BroadcastReceiver() {
             override fun onReceive(context: Context, intent: Intent) {
                 val action = intent.getAction()
-                if (Intent.ACTION_SCREEN_OFF.equals(action)) {
+                if (action == Intent.ACTION_SCREEN_OFF ||
+                    action == Intent.ACTION_DREAMING_STARTED) {
                     finish()
                 }
             }
@@ -114,6 +138,7 @@
 
         val filter = IntentFilter()
         filter.addAction(Intent.ACTION_SCREEN_OFF)
+        filter.addAction(Intent.ACTION_DREAMING_STARTED)
         broadcastDispatcher.registerReceiver(broadcastReceiver, filter)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
index 822f8f2..c1cfbcb 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt
@@ -27,6 +27,7 @@
     companion object {
         public const val TAG = "ControlsUiController"
         public const val EXTRA_ANIMATE = "extra_animate"
+        public const val EXIT_TO_DREAM = "extra_exit_to_dream"
     }
 
     fun show(parent: ViewGroup, onDismiss: Runnable, activityContext: Context)
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 721c0ba..09743ef 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.accessibility.SystemActions
 import com.android.systemui.accessibility.WindowMagnification
 import com.android.systemui.biometrics.AuthController
+import com.android.systemui.biometrics.UdfpsOverlay
 import com.android.systemui.clipboardoverlay.ClipboardListener
 import com.android.systemui.dagger.qualifiers.PerUser
 import com.android.systemui.globalactions.GlobalActionsComponent
@@ -218,6 +219,12 @@
     @ClassKey(KeyguardLiftController::class)
     abstract fun bindKeyguardLiftController(sysui: KeyguardLiftController): CoreStartable
 
+    /** Inject into UdfpsOverlay.  */
+    @Binds
+    @IntoMap
+    @ClassKey(UdfpsOverlay::class)
+    abstract fun bindUdfpsOverlay(sysui: UdfpsOverlay): CoreStartable
+
     /** Inject into MediaTttSenderCoordinator. */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
index cc57662..0e1bfba 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
@@ -19,10 +19,10 @@
 import android.view.Display
 import com.android.systemui.doze.DozeLog.Reason
 import com.android.systemui.doze.DozeLog.reasonToString
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.log.dagger.DozeLog
 import com.android.systemui.statusbar.policy.DevicePostureController
 import java.text.SimpleDateFormat
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
index 0ccb222..cedd850a 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
@@ -210,7 +210,8 @@
 
             final Intent intent = new Intent(mContext, ControlsActivity.class)
                     .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK)
-                    .putExtra(ControlsUiController.EXTRA_ANIMATE, true);
+                    .putExtra(ControlsUiController.EXTRA_ANIMATE, true)
+                    .putExtra(ControlsUiController.EXIT_TO_DREAM, true);
 
             final ActivityLaunchAnimator.Controller controller =
                     v != null ? ActivityLaunchAnimator.Controller.fromView(v, null /* cujType */)
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
index 08ef8f3..478f861 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
@@ -24,7 +24,7 @@
 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL
 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_HIGH
 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager
 import java.io.PrintWriter
 import javax.inject.Inject
@@ -235,6 +235,7 @@
         pw.println("$ <invocation> buffers")
         pw.println("$ <invocation> bugreport-critical")
         pw.println("$ <invocation> bugreport-normal")
+        pw.println("$ <invocation> config")
         pw.println()
 
         pw.println("Targets can be listed:")
@@ -313,13 +314,21 @@
         const val PRIORITY_ARG_CRITICAL = "CRITICAL"
         const val PRIORITY_ARG_HIGH = "HIGH"
         const val PRIORITY_ARG_NORMAL = "NORMAL"
+        const val PROTO = "--sysui_proto"
     }
 }
 
 private val PRIORITY_OPTIONS =
         arrayOf(PRIORITY_ARG_CRITICAL, PRIORITY_ARG_HIGH, PRIORITY_ARG_NORMAL)
 
-private val COMMANDS = arrayOf("bugreport-critical", "bugreport-normal", "buffers", "dumpables")
+private val COMMANDS = arrayOf(
+        "bugreport-critical",
+        "bugreport-normal",
+        "buffers",
+        "dumpables",
+        "config",
+        "help"
+)
 
 private class ParsedArgs(
     val rawArgs: Array<String>,
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
index cca04da..dbca651 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
@@ -18,7 +18,7 @@
 
 import android.util.ArrayMap
 import com.android.systemui.Dumpable
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.plugins.log.LogBuffer
 import java.io.PrintWriter
 import javax.inject.Inject
 import javax.inject.Singleton
diff --git a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt
index 0eab1af..8299b13 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/LogBufferEulogizer.kt
@@ -19,7 +19,7 @@
 import android.content.Context
 import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.util.io.Files
 import com.android.systemui.util.time.SystemClock
 import java.io.IOException
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 9beb1e9..46cfab9 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -73,7 +73,11 @@
 
     public static final UnreleasedFlag NOTIFICATION_DISMISSAL_FADE = new UnreleasedFlag(113, true);
 
-    // next id: 114
+    public static final UnreleasedFlag STABILITY_INDEX_FIX = new UnreleasedFlag(114, true);
+
+    public static final UnreleasedFlag SEMI_STABLE_SORT = new UnreleasedFlag(115, true);
+
+    // next id: 116
 
     /***************************************/
     // 200 - keyguard/lockscreen
@@ -111,8 +115,8 @@
      * <p>If this is {@code false}, the interactor and repo skip the controller and directly access
      * the framework APIs.
      */
-    public static final ReleasedFlag USER_INTERACTOR_AND_REPO_USE_CONTROLLER =
-            new ReleasedFlag(210);
+    public static final UnreleasedFlag USER_INTERACTOR_AND_REPO_USE_CONTROLLER =
+            new UnreleasedFlag(210);
 
     /**
      * Whether `UserSwitcherController` should use the user interactor.
@@ -123,7 +127,13 @@
      * <p>Note: do not set this to true if {@link #USER_INTERACTOR_AND_REPO_USE_CONTROLLER} is
      * {@code true} as it would created a cycle between controller -> interactor -> controller.
      */
-    public static final UnreleasedFlag USER_CONTROLLER_USES_INTERACTOR = new UnreleasedFlag(211);
+    public static final ReleasedFlag USER_CONTROLLER_USES_INTERACTOR = new ReleasedFlag(211);
+
+    /**
+     * Whether the clock on a wide lock screen should use the new "stepping" animation for moving
+     * the digits when the clock moves.
+     */
+    public static final UnreleasedFlag STEP_CLOCK_ANIMATION = new UnreleasedFlag(212);
 
     /***************************************/
     // 300 - power menu
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
index 5651399..f9e341c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
@@ -19,6 +19,9 @@
 import android.app.ActivityManager
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogcatEchoTracker
+
 import javax.inject.Inject
 
 @SysUISingleton
@@ -26,7 +29,7 @@
     private val dumpManager: DumpManager,
     private val logcatEchoTracker: LogcatEchoTracker
 ) {
-    /* limit the size of maxPoolSize for low ram (Go) devices */
+    /* limitiometricMessageDeferralLogger the size of maxPoolSize for low ram (Go) devices */
     private fun adjustMaxSize(requestedMaxSize: Int): Int {
         return if (ActivityManager.isLowRamDeviceStatic()) {
             minOf(requestedMaxSize, 20) /* low ram max log size*/
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java
index 7f1ad6d..eeadf40 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BiometricMessagesLog.java
@@ -23,7 +23,7 @@
 import javax.inject.Qualifier;
 
 /**
- * A {@link com.android.systemui.log.LogBuffer} for BiometricMessages processing such as
+ * A {@link com.android.systemui.plugins.log.LogBuffer} for BiometricMessages processing such as
  * {@link com.android.systemui.biometrics.FaceHelpMessageDeferral}
  */
 @Qualifier
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java
index 7d1f1c2..5cca1ab 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BroadcastDispatcherLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java
index 9ca0293..1d016d8 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/CollapsedSbFragmentLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java
index 7c5f402..c9f78bc 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/DozeLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java
index 08d969b..76d20be 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LSShadeTransitionLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 28aa19e..00bf210 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -22,11 +22,11 @@
 
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.log.LogBuffer;
 import com.android.systemui.log.LogBufferFactory;
-import com.android.systemui.log.LogcatEchoTracker;
-import com.android.systemui.log.LogcatEchoTrackerDebug;
-import com.android.systemui.log.LogcatEchoTrackerProd;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogcatEchoTracker;
+import com.android.systemui.plugins.log.LogcatEchoTrackerDebug;
+import com.android.systemui.plugins.log.LogcatEchoTrackerProd;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.util.Compile;
 
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java
index 1d7ba94..90ced02 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
index b03655a..e5ac3e2 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java
index c67d8be..73690ab 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaMuteAwaitLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
index 53963fc..99ec05b 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java
index 5c572e8..1570d43 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttReceiverLogBuffer.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java
index edab8c3..bf216c6 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTttSenderLogBuffer.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
index 75a34fc..8c904ea 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java
index b1c6dcf..6d91f0c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NearbyMediaDevicesLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java
index 20fc6ff..26af496 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java
index fcc184a..61daf9c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java
index 760fbf3..a59afa0 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationInterruptLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java
index a0b6864..6f8ea7f 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java
index 8c8753a..835d349 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationRenderLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java
index 7259eeb..6e2bd7b 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationSectionLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java
index e96e532..77b1bf5 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/PrivacyLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java
index 557a254..9fd166b 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSFragmentDisableLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java
index dd5010c..dd168ba 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/QSLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java
index bd0d298..d24bfcb 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
index b237f2d..67cdb72 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java
index f26b316..af0f7c5 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarNetworkControllerLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
index dd68375..4c276e2 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java
index 8671dbf..ba8b27c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java
@@ -18,7 +18,7 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt
index b1018f9..d40624b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.media
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.MediaCarouselControllerLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 /** A debug logger for [MediaCarouselController]. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
index b52565d..cc06b6c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
@@ -33,7 +33,6 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.people.widget.PeopleSpaceWidgetProvider.EXTRA_USER_HANDLE
 import com.android.systemui.tuner.TunerService
 import com.android.systemui.util.Utils
 import com.android.systemui.util.time.SystemClock
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt
index d9c58c0..8c9e2d8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt
@@ -18,11 +18,10 @@
 
 import android.media.session.PlaybackState
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.MediaTimeoutListenerLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
-
 private const val TAG = "MediaTimeout"
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt
index 73868189..51c658c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.media
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.MediaViewLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 private const val TAG = "MediaView"
diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt
index 41f7354..a9c5c61 100644
--- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt
@@ -18,9 +18,9 @@
 
 import android.content.ComponentName
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.MediaBrowserLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 /** A logger for events in [ResumeMediaBrowser]. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
index a8a8433..e15e2d3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
@@ -17,7 +17,6 @@
 package com.android.systemui.media.dagger;
 
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.log.LogBuffer;
 import com.android.systemui.log.dagger.MediaTttReceiverLogBuffer;
 import com.android.systemui.log.dagger.MediaTttSenderLogBuffer;
 import com.android.systemui.media.MediaDataManager;
@@ -33,6 +32,7 @@
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger;
 import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger;
 import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger;
+import com.android.systemui.plugins.log.LogBuffer;
 
 import java.util.Optional;
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt
index 78f4e01..5ace3ea 100644
--- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitLogger.kt
@@ -1,9 +1,9 @@
 package com.android.systemui.media.muteawait
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.MediaMuteAwaitLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 /** Log messages for [MediaMuteAwaitConnectionManager]. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt
index 46b2cc14..78408fc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesLogger.kt
@@ -1,9 +1,9 @@
 package com.android.systemui.media.nearby
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NearbyMediaDevicesLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 /** Log messages for [NearbyMediaDevicesManager]. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
index b565f3c..38c971e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
@@ -16,8 +16,8 @@
 
 package com.android.systemui.media.taptotransfer.common
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.temporarydisplay.TemporaryViewLogger
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
index 1ea9347..03503fd 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
@@ -17,10 +17,10 @@
 package com.android.systemui.privacy.logging
 
 import android.permission.PermissionGroupUsage
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogMessage
 import com.android.systemui.log.dagger.PrivacyLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogMessage
 import com.android.systemui.privacy.PrivacyDialog
 import com.android.systemui.privacy.PrivacyItem
 import java.text.SimpleDateFormat
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
index e5d86cc..025fb22 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentDisableFlagsLogger.kt
@@ -1,8 +1,8 @@
 package com.android.systemui.qs
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.QSFragmentDisableLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
index 4cacbba..5d03da3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
@@ -35,6 +35,7 @@
 
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.qs.QSTileHost;
 import com.android.systemui.settings.UserTracker;
@@ -53,6 +54,7 @@
 /**
  * Runs the day-to-day operations of which tiles should be bound and when.
  */
+@SysUISingleton
 public class TileServices extends IQSService.Stub {
     static final int DEFAULT_MAX_BOUND = 3;
     static final int REDUCED_MAX_BOUND = 1;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
index 6038006..931dc8d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -17,12 +17,12 @@
 package com.android.systemui.qs.logging
 
 import android.service.quicksettings.Tile
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.VERBOSE
-import com.android.systemui.log.LogMessage
 import com.android.systemui.log.dagger.QSLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
+import com.android.systemui.plugins.log.LogMessage
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.statusbar.StatusBarState
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
index d2d5063..b6b657e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
@@ -26,6 +26,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.MetricsLogger;
@@ -43,6 +44,9 @@
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.user.data.source.UserRecord;
 
+import java.util.List;
+import java.util.stream.Collectors;
+
 import javax.inject.Inject;
 
 /**
@@ -83,6 +87,13 @@
         private final FalsingManager mFalsingManager;
         private @Nullable UserSwitchDialogController.DialogShower mDialogShower;
 
+        @NonNull
+        @Override
+        protected List<UserRecord> getUsers() {
+            return super.getUsers().stream().filter(
+                    userRecord -> !userRecord.isManageUsers).collect(Collectors.toList());
+        }
+
         @Inject
         public Adapter(Context context, UserSwitcherController controller,
                 UiEventLogger uiEventLogger, FalsingManager falsingManager) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
index a494f42..6b540aa 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
@@ -292,6 +292,7 @@
             clock.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
                 val newPivot = if (v.isLayoutRtl) v.width.toFloat() else 0f
                 v.pivotX = newPivot
+                v.pivotY = v.height.toFloat() / 2
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt
index 07e8b9f..754036d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NPVCDownEventState.kt
@@ -16,7 +16,7 @@
 import android.view.MotionEvent
 import com.android.systemui.dump.DumpsysTableLogger
 import com.android.systemui.dump.Row
-import com.android.systemui.util.collection.RingBuffer
+import com.android.systemui.plugins.util.RingBuffer
 import java.text.SimpleDateFormat
 import java.util.Locale
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 20f0655..a4c0ea1 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -79,7 +79,10 @@
 import android.os.VibrationEffect;
 import android.provider.Settings;
 import android.transition.ChangeBounds;
+import android.transition.Transition;
 import android.transition.TransitionManager;
+import android.transition.TransitionSet;
+import android.transition.TransitionValues;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.MathUtils;
@@ -1667,9 +1670,40 @@
                     // horizontally properly.
                     transition.excludeTarget(R.id.status_view_media_container, true);
                 }
+
                 transition.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
                 transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
-                TransitionManager.beginDelayedTransition(mNotificationContainerParent, transition);
+
+                boolean customClockAnimation =
+                            mKeyguardStatusViewController.getClockAnimations() != null
+                            && mKeyguardStatusViewController.getClockAnimations()
+                                    .getHasCustomPositionUpdatedAnimation();
+
+                if (mFeatureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION) && customClockAnimation) {
+                    // Find the clock, so we can exclude it from this transition.
+                    FrameLayout clockContainerView =
+                            mView.findViewById(R.id.lockscreen_clock_view_large);
+                    View clockView = clockContainerView.getChildAt(0);
+
+                    transition.excludeTarget(clockView, /* exclude= */ true);
+
+                    TransitionSet set = new TransitionSet();
+                    set.addTransition(transition);
+
+                    SplitShadeTransitionAdapter adapter =
+                            new SplitShadeTransitionAdapter(mKeyguardStatusViewController);
+
+                    // Use linear here, so the actual clock can pick its own interpolator.
+                    adapter.setInterpolator(Interpolators.LINEAR);
+                    adapter.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+                    adapter.addTarget(clockView);
+                    set.addTransition(adapter);
+
+                    TransitionManager.beginDelayedTransition(mNotificationContainerParent, set);
+                } else {
+                    TransitionManager.beginDelayedTransition(
+                            mNotificationContainerParent, transition);
+                }
             }
 
             constraintSet.applyTo(mNotificationContainerParent);
@@ -2093,7 +2127,8 @@
         animator.start();
     }
 
-    private void onFlingEnd(boolean cancelled) {
+    @VisibleForTesting
+    void onFlingEnd(boolean cancelled) {
         mIsFlinging = false;
         // No overshoot when the animation ends
         setOverExpansionInternal(0, false /* isFromGesture */);
@@ -3834,12 +3869,14 @@
         }
     }
 
-    private void setIsClosing(boolean isClosing) {
+    @VisibleForTesting
+    void setIsClosing(boolean isClosing) {
         boolean wasClosing = isClosing();
         mClosing = isClosing;
         if (wasClosing != isClosing) {
             mPanelEventsEmitter.notifyPanelCollapsingChanged(isClosing);
         }
+        mAmbientState.setIsClosing(isClosing);
     }
 
     private void updateDozingVisibilities(boolean animate) {
@@ -4630,14 +4667,16 @@
         Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
     }
 
-    private void notifyExpandingStarted() {
+    @VisibleForTesting
+    void notifyExpandingStarted() {
         if (!mExpanding) {
             mExpanding = true;
             onExpandingStarted();
         }
     }
 
-    private void notifyExpandingFinished() {
+    @VisibleForTesting
+    void notifyExpandingFinished() {
         endClosing();
         if (mExpanding) {
             mExpanding = false;
@@ -6251,4 +6290,54 @@
             loadDimens();
         }
     }
+
+    static class SplitShadeTransitionAdapter extends Transition {
+        private static final String PROP_BOUNDS = "splitShadeTransitionAdapter:bounds";
+        private static final String[] TRANSITION_PROPERTIES = { PROP_BOUNDS };
+
+        private final KeyguardStatusViewController mController;
+
+        SplitShadeTransitionAdapter(KeyguardStatusViewController controller) {
+            mController = controller;
+        }
+
+        private void captureValues(TransitionValues transitionValues) {
+            Rect boundsRect = new Rect();
+            boundsRect.left = transitionValues.view.getLeft();
+            boundsRect.top = transitionValues.view.getTop();
+            boundsRect.right = transitionValues.view.getRight();
+            boundsRect.bottom = transitionValues.view.getBottom();
+            transitionValues.values.put(PROP_BOUNDS, boundsRect);
+        }
+
+        @Override
+        public void captureEndValues(TransitionValues transitionValues) {
+            captureValues(transitionValues);
+        }
+
+        @Override
+        public void captureStartValues(TransitionValues transitionValues) {
+            captureValues(transitionValues);
+        }
+
+        @Override
+        public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
+                TransitionValues endValues) {
+            ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
+
+            Rect from = (Rect) startValues.values.get(PROP_BOUNDS);
+            Rect to = (Rect) endValues.values.get(PROP_BOUNDS);
+
+            anim.addUpdateListener(
+                    animation -> mController.getClockAnimations().onPositionUpdated(
+                            from, to, animation.getAnimatedFraction()));
+
+            return anim;
+        }
+
+        @Override
+        public String[] getTransitionProperties() {
+            return TRANSITION_PROPERTIES;
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
index 7bee0ba..2b788d8 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
@@ -1,10 +1,10 @@
 package com.android.systemui.shade
 
 import android.view.MotionEvent
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogMessage
 import com.android.systemui.log.dagger.ShadeLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogMessage
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
 
@@ -12,64 +12,69 @@
 
 /** Lightweight logging utility for the Shade. */
 class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) {
-  fun v(@CompileTimeConstant msg: String) {
-    buffer.log(TAG, LogLevel.VERBOSE, msg)
-  }
+    fun v(@CompileTimeConstant msg: String) {
+        buffer.log(TAG, LogLevel.VERBOSE, msg)
+    }
 
-  private inline fun log(
-      logLevel: LogLevel,
-      initializer: LogMessage.() -> Unit,
-      noinline printer: LogMessage.() -> String
-  ) {
-    buffer.log(TAG, logLevel, initializer, printer)
-  }
+    private inline fun log(
+        logLevel: LogLevel,
+        initializer: LogMessage.() -> Unit,
+        noinline printer: LogMessage.() -> String
+    ) {
+        buffer.log(TAG, logLevel, initializer, printer)
+    }
 
-  fun onQsInterceptMoveQsTrackingEnabled(h: Float) {
-    log(
-        LogLevel.VERBOSE,
-        { double1 = h.toDouble() },
-        { "onQsIntercept: move action, QS tracking enabled. h = $double1" })
-  }
+    fun onQsInterceptMoveQsTrackingEnabled(h: Float) {
+        log(
+            LogLevel.VERBOSE,
+            { double1 = h.toDouble() },
+            { "onQsIntercept: move action, QS tracking enabled. h = $double1" }
+        )
+    }
 
-  fun logQsTrackingNotStarted(
-      initialTouchY: Float,
-      y: Float,
-      h: Float,
-      touchSlop: Float,
-      qsExpanded: Boolean,
-      collapsedOnDown: Boolean,
-      keyguardShowing: Boolean,
-      qsExpansionEnabled: Boolean
-  ) {
-    log(
-        LogLevel.VERBOSE,
-        {
-          int1 = initialTouchY.toInt()
-          int2 = y.toInt()
-          long1 = h.toLong()
-          double1 = touchSlop.toDouble()
-          bool1 = qsExpanded
-          bool2 = collapsedOnDown
-          bool3 = keyguardShowing
-          bool4 = qsExpansionEnabled
-        },
-        {
-          "QsTrackingNotStarted: initTouchY=$int1,y=$int2,h=$long1,slop=$double1,qsExpanded=" +
-              "$bool1,collapsedDown=$bool2,keyguardShowing=$bool3,qsExpansion=$bool4"
-        })
-  }
+    fun logQsTrackingNotStarted(
+        initialTouchY: Float,
+        y: Float,
+        h: Float,
+        touchSlop: Float,
+        qsExpanded: Boolean,
+        collapsedOnDown: Boolean,
+        keyguardShowing: Boolean,
+        qsExpansionEnabled: Boolean
+    ) {
+        log(
+            LogLevel.VERBOSE,
+            {
+                int1 = initialTouchY.toInt()
+                int2 = y.toInt()
+                long1 = h.toLong()
+                double1 = touchSlop.toDouble()
+                bool1 = qsExpanded
+                bool2 = collapsedOnDown
+                bool3 = keyguardShowing
+                bool4 = qsExpansionEnabled
+            },
+            {
+                "QsTrackingNotStarted: initTouchY=$int1,y=$int2,h=$long1,slop=$double1,qsExpanded" +
+                    "=$bool1,collapsedDown=$bool2,keyguardShowing=$bool3,qsExpansion=$bool4"
+            }
+        )
+    }
 
-  fun logMotionEvent(event: MotionEvent, message: String) {
-    log(
-        LogLevel.VERBOSE,
-        {
-          str1 = message
-          long1 = event.eventTime
-          long2 = event.downTime
-          int1 = event.action
-          int2 = event.classification
-          double1 = event.y.toDouble()
-        },
-        { "$str1\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,classification=$int2" })
-  }
+    fun logMotionEvent(event: MotionEvent, message: String) {
+        log(
+            LogLevel.VERBOSE,
+            {
+                str1 = message
+                long1 = event.eventTime
+                long2 = event.downTime
+                int1 = event.action
+                int2 = event.classification
+                double1 = event.y.toDouble()
+            },
+            {
+                "$str1\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,class=$int2"
+            }
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
index 7f7ff9cf..90c52bd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.statusbar
 
 import android.app.PendingIntent
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotifInteractionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
index ea7ec4f..450b757 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -71,9 +71,9 @@
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.log.LogBuffer;
-import com.android.systemui.log.LogLevel;
 import com.android.systemui.log.dagger.StatusBarNetworkControllerLog;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogLevel;
 import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
 import com.android.systemui.settings.CurrentUserTracker;
 import com.android.systemui.statusbar.policy.ConfigurationController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
index 17feaa8..9bdff92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.gesture
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.SwipeStatusBarAwayLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 /** Log messages for [SwipeStatusBarAwayGestureHandler]. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index dfba8cd..fc984618 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -119,6 +119,7 @@
                     regionSamplingEnabled,
                     updateFun
             )
+            initializeTextColors(regionSamplingInstance)
             regionSamplingInstance.startRegionSampler()
             regionSamplingInstances.put(v, regionSamplingInstance)
             connectSession()
@@ -362,18 +363,20 @@
         }
     }
 
+    private fun initializeTextColors(regionSamplingInstance: RegionSamplingInstance) {
+        val lightThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_LightWallpaper)
+        val darkColor = Utils.getColorAttrDefaultColor(lightThemeContext, R.attr.wallpaperTextColor)
+
+        val darkThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI)
+        val lightColor = Utils.getColorAttrDefaultColor(darkThemeContext, R.attr.wallpaperTextColor)
+
+        regionSamplingInstance.setForegroundColors(lightColor, darkColor)
+    }
+
     private fun updateTextColorFromRegionSampler() {
         smartspaceViews.forEach {
-            val isRegionDark = regionSamplingInstances.getValue(it).currentRegionDarkness()
-            val themeID = if (isRegionDark.isDark) {
-                R.style.Theme_SystemUI
-            } else {
-                R.style.Theme_SystemUI_LightWallpaper
-            }
-            val themedContext = ContextThemeWrapper(context, themeID)
-            val wallpaperTextColor =
-                    Utils.getColorAttrDefaultColor(themedContext, R.attr.wallpaperTextColor)
-            it.setPrimaryTextColor(wallpaperTextColor)
+            val textColor = regionSamplingInstances.getValue(it).currentForegroundColor()
+            it.setPrimaryTextColor(textColor)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
index 7fbdd35..e3e8a99 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
@@ -53,4 +53,12 @@
 
     fun fullScreenIntentRequiresKeyguard(): Boolean =
         featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD)
+
+    val isStabilityIndexFixEnabled: Boolean by lazy {
+        featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX)
+    }
+
+    val isSemiStableSortEnabled: Boolean by lazy {
+        featureFlags.isEnabled(Flags.SEMI_STABLE_SORT)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
index ad3dfed..3058fbb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotifInteractionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
index f8449ae..84ab0d1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
@@ -68,6 +68,9 @@
      */
     var stableIndex: Int = -1
 
+    /** Access the index of the [section] or -1 if the entry does not have one */
+    val sectionIndex: Int get() = section?.index ?: -1
+
     /** Copies the state of another instance. */
     fun clone(other: ListAttachState) {
         parent = other.parent
@@ -95,11 +98,13 @@
      * This can happen if the entry is removed from a group that was broken up or if the entry was
      * filtered out during any of the filtering steps.
      */
-    fun detach() {
+    fun detach(includingStableIndex: Boolean) {
         parent = null
         section = null
         promoter = null
-        // stableIndex = -1  // TODO(b/241229236): Clear this once we fix the stability fragility
+        if (includingStableIndex) {
+            stableIndex = -1
+        }
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
index e129ee4..3ae2545 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
@@ -54,6 +54,9 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener;
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener;
 import com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState;
+import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort;
+import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort.StableOrder;
+import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper;
 import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifStabilityManager;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator;
@@ -96,11 +99,14 @@
     // used exclusivly by ShadeListBuilder#notifySectionEntriesUpdated
     // TODO replace temp with collection pool for readability
     private final ArrayList<ListEntry> mTempSectionMembers = new ArrayList<>();
+    private NotifPipelineFlags mFlags;
     private final boolean mAlwaysLogList;
 
     private List<ListEntry> mNotifList = new ArrayList<>();
     private List<ListEntry> mNewNotifList = new ArrayList<>();
 
+    private final SemiStableSort mSemiStableSort = new SemiStableSort();
+    private final StableOrder<ListEntry> mStableOrder = this::getStableOrderRank;
     private final PipelineState mPipelineState = new PipelineState();
     private final Map<String, GroupEntry> mGroups = new ArrayMap<>();
     private Collection<NotificationEntry> mAllEntries = Collections.emptyList();
@@ -141,6 +147,7 @@
     ) {
         mSystemClock = systemClock;
         mLogger = logger;
+        mFlags = flags;
         mAlwaysLogList = flags.isDevLoggingEnabled();
         mInteractionTracker = interactionTracker;
         mChoreographer = pipelineChoreographer;
@@ -527,7 +534,7 @@
             List<NotifFilter> filters) {
         Trace.beginSection("ShadeListBuilder.filterNotifs");
         final long now = mSystemClock.uptimeMillis();
-        for (ListEntry entry : entries)  {
+        for (ListEntry entry : entries) {
             if (entry instanceof GroupEntry) {
                 final GroupEntry groupEntry = (GroupEntry) entry;
 
@@ -958,7 +965,8 @@
      * filtered out during any of the filtering steps.
      */
     private void annulAddition(ListEntry entry) {
-        entry.getAttachState().detach();
+        // NOTE(b/241229236): Don't clear stableIndex until we fix stability fragility
+        entry.getAttachState().detach(/* includingStableIndex= */ mFlags.isSemiStableSortEnabled());
     }
 
     private void assignSections() {
@@ -978,7 +986,16 @@
 
     private void sortListAndGroups() {
         Trace.beginSection("ShadeListBuilder.sortListAndGroups");
-        // Assign sections to top-level elements and sort their children
+        if (mFlags.isSemiStableSortEnabled()) {
+            sortWithSemiStableSort();
+        } else {
+            sortWithLegacyStability();
+        }
+        Trace.endSection();
+    }
+
+    private void sortWithLegacyStability() {
+        // Sort all groups and the top level list
         for (ListEntry entry : mNotifList) {
             if (entry instanceof GroupEntry) {
                 GroupEntry parent = (GroupEntry) entry;
@@ -991,16 +1008,15 @@
         // Check for suppressed order changes
         if (!getStabilityManager().isEveryChangeAllowed()) {
             mForceReorderable = true;
-            boolean isSorted = isShadeSorted();
+            boolean isSorted = isShadeSortedLegacy();
             mForceReorderable = false;
             if (!isSorted) {
                 getStabilityManager().onEntryReorderSuppressed();
             }
         }
-        Trace.endSection();
     }
 
-    private boolean isShadeSorted() {
+    private boolean isShadeSortedLegacy() {
         if (!isSorted(mNotifList, mTopLevelComparator)) {
             return false;
         }
@@ -1014,6 +1030,43 @@
         return true;
     }
 
+    private void sortWithSemiStableSort() {
+        // Sort each group's children
+        boolean allSorted = true;
+        for (ListEntry entry : mNotifList) {
+            if (entry instanceof GroupEntry) {
+                GroupEntry parent = (GroupEntry) entry;
+                allSorted &= sortGroupChildren(parent.getRawChildren());
+            }
+        }
+        // Sort each section within the top level list
+        mNotifList.sort(mTopLevelComparator);
+        if (!getStabilityManager().isEveryChangeAllowed()) {
+            for (List<ListEntry> subList : getSectionSubLists(mNotifList)) {
+                allSorted &= mSemiStableSort.stabilizeTo(subList, mStableOrder, mNewNotifList);
+            }
+            applyNewNotifList();
+        }
+        assignIndexes(mNotifList);
+        if (!allSorted) {
+            // Report suppressed order changes
+            getStabilityManager().onEntryReorderSuppressed();
+        }
+    }
+
+    private Iterable<List<ListEntry>> getSectionSubLists(List<ListEntry> entries) {
+        return ShadeListBuilderHelper.INSTANCE.getSectionSubLists(entries);
+    }
+
+    private boolean sortGroupChildren(List<NotificationEntry> entries) {
+        if (getStabilityManager().isEveryChangeAllowed()) {
+            entries.sort(mGroupChildrenComparator);
+            return true;
+        } else {
+            return mSemiStableSort.sort(entries, mStableOrder, mGroupChildrenComparator);
+        }
+    }
+
     /** Determine whether the items in the list are sorted according to the comparator */
     @VisibleForTesting
     public static <T> boolean isSorted(List<T> items, Comparator<? super T> comparator) {
@@ -1036,27 +1089,41 @@
     /**
      * Assign the index of each notification relative to the total order
      */
-    private static void assignIndexes(List<ListEntry> notifList) {
+    private void assignIndexes(List<ListEntry> notifList) {
         if (notifList.size() == 0) return;
         NotifSection currentSection = requireNonNull(notifList.get(0).getSection());
         int sectionMemberIndex = 0;
         for (int i = 0; i < notifList.size(); i++) {
-            ListEntry entry = notifList.get(i);
+            final ListEntry entry = notifList.get(i);
             NotifSection section = requireNonNull(entry.getSection());
             if (section.getIndex() != currentSection.getIndex()) {
                 sectionMemberIndex = 0;
                 currentSection = section;
             }
-            entry.getAttachState().setStableIndex(sectionMemberIndex);
-            if (entry instanceof GroupEntry) {
-                GroupEntry parent = (GroupEntry) entry;
-                for (int j = 0; j < parent.getChildren().size(); j++) {
-                    entry = parent.getChildren().get(j);
-                    entry.getAttachState().setStableIndex(sectionMemberIndex);
-                    sectionMemberIndex++;
+            if (mFlags.isStabilityIndexFixEnabled()) {
+                entry.getAttachState().setStableIndex(sectionMemberIndex++);
+                if (entry instanceof GroupEntry) {
+                    final GroupEntry parent = (GroupEntry) entry;
+                    final NotificationEntry summary = parent.getSummary();
+                    if (summary != null) {
+                        summary.getAttachState().setStableIndex(sectionMemberIndex++);
+                    }
+                    for (NotificationEntry child : parent.getChildren()) {
+                        child.getAttachState().setStableIndex(sectionMemberIndex++);
+                    }
                 }
+            } else {
+                // This old implementation uses the same index number for the group as the first
+                // child, and fails to assign an index to the summary.  Remove once tested.
+                entry.getAttachState().setStableIndex(sectionMemberIndex);
+                if (entry instanceof GroupEntry) {
+                    final GroupEntry parent = (GroupEntry) entry;
+                    for (NotificationEntry child : parent.getChildren()) {
+                        child.getAttachState().setStableIndex(sectionMemberIndex++);
+                    }
+                }
+                sectionMemberIndex++;
             }
-            sectionMemberIndex++;
         }
     }
 
@@ -1196,7 +1263,7 @@
                 o2.getSectionIndex());
         if (cmp != 0) return cmp;
 
-        cmp = Integer.compare(
+        cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare(
                 getStableOrderIndex(o1),
                 getStableOrderIndex(o2));
         if (cmp != 0) return cmp;
@@ -1225,7 +1292,7 @@
 
 
     private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> {
-        int cmp = Integer.compare(
+        int cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare(
                 getStableOrderIndex(o1),
                 getStableOrderIndex(o2));
         if (cmp != 0) return cmp;
@@ -1256,9 +1323,25 @@
             // let the stability manager constrain or allow reordering
             return -1;
         }
+        // NOTE(b/241229236): Can't use cleared section index until we fix stability fragility
         return entry.getPreviousAttachState().getStableIndex();
     }
 
+    @Nullable
+    private Integer getStableOrderRank(ListEntry entry) {
+        if (getStabilityManager().isEntryReorderingAllowed(entry)) {
+            // let the stability manager constrain or allow reordering
+            return null;
+        }
+        if (entry.getAttachState().getSectionIndex()
+                != entry.getPreviousAttachState().getSectionIndex()) {
+            // stable index is only valid within the same section; otherwise we allow reordering
+            return null;
+        }
+        final int stableIndex = entry.getPreviousAttachState().getStableIndex();
+        return stableIndex == -1 ? null : stableIndex;
+    }
+
     private boolean applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters) {
         final NotifFilter filter = findRejectingFilter(entry, now, filters);
         entry.getAttachState().setExcludingFilter(filter);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt
index 211e374..68d1319 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescerLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.collection.coalescer
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 class GroupCoalescerLogger @Inject constructor(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt
index e8f352f..2919def 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinatorLogger.kt
@@ -1,8 +1,8 @@
 package com.android.systemui.statusbar.notification.collection.coordinator
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.row.NotificationGuts
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
index 8625cdb..dfaa291 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
@@ -1,9 +1,10 @@
 package com.android.systemui.statusbar.notification.collection.coordinator
 
 import android.util.Log
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 private const val TAG = "HeadsUpCoordinator"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
index 93146f9..6e76691 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
@@ -407,10 +407,7 @@
             mLogger.logGroupInflationTookTooLong(group);
             return false;
         }
-        // Only delay release if the summary is not inflated.
-        // TODO(253454977): Once we ensure that all other pipeline filtering and pruning has been
-        //  done by this point, we can revert back to checking for mInflatingNotifs.contains(...)
-        if (!isInflated(group.getSummary())) {
+        if (mInflatingNotifs.contains(group.getSummary())) {
             mLogger.logDelayingGroupRelease(group, group.getSummary());
             return true;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
index c4f4ed5..9558f47 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt
index c687e1b..d804454 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ShadeEventCoordinatorLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 private const val TAG = "ShadeEventCoordinator"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt
new file mode 100644
index 0000000..9ec8e07
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.listbuilder
+
+import androidx.annotation.VisibleForTesting
+import kotlin.math.sign
+
+class SemiStableSort {
+    val preallocatedWorkspace by lazy { ArrayList<Any>() }
+    val preallocatedAdditions by lazy { ArrayList<Any>() }
+    val preallocatedMapToIndex by lazy { HashMap<Any, Int>() }
+    val preallocatedMapToIndexComparator: Comparator<Any> by lazy {
+        Comparator.comparingInt { item -> preallocatedMapToIndex[item] ?: -1 }
+    }
+
+    /**
+     * Sort the given [items] such that items which have a [stableOrder] will all be in that order,
+     * items without a [stableOrder] will be sorted according to the comparator, and the two sets of
+     * items will be combined to have the fewest elements out of order according to the [comparator]
+     * . The result will be placed into the original [items] list.
+     */
+    fun <T : Any> sort(
+        items: MutableList<T>,
+        stableOrder: StableOrder<in T>,
+        comparator: Comparator<in T>,
+    ): Boolean =
+        withWorkspace<T, Boolean> { workspace ->
+            val ordered =
+                sortTo(
+                    items,
+                    stableOrder,
+                    comparator,
+                    workspace,
+                )
+            items.clear()
+            items.addAll(workspace)
+            return ordered
+        }
+
+    /**
+     * Sort the given [items] such that items which have a [stableOrder] will all be in that order,
+     * items without a [stableOrder] will be sorted according to the comparator, and the two sets of
+     * items will be combined to have the fewest elements out of order according to the [comparator]
+     * . The result will be put into [output].
+     */
+    fun <T : Any> sortTo(
+        items: Iterable<T>,
+        stableOrder: StableOrder<in T>,
+        comparator: Comparator<in T>,
+        output: MutableList<T>,
+    ): Boolean {
+        if (DEBUG) println("\n> START from ${items.map { it to stableOrder.getRank(it) }}")
+        // If array already has elements, use subList to ensure we only append
+        val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size)
+        items.filterTo(result) { stableOrder.getRank(it) != null }
+        result.sortBy { stableOrder.getRank(it)!! }
+        val isOrdered = result.isSorted(comparator)
+        withAdditions<T> { additions ->
+            items.filterTo(additions) { stableOrder.getRank(it) == null }
+            additions.sortWith(comparator)
+            insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator)
+        }
+        return isOrdered
+    }
+
+    /**
+     * Rearrange the [sortedItems] to enforce that items are in the [stableOrder], and store the
+     * result in [output]. Items with a [stableOrder] will be in that order, items without a
+     * [stableOrder] will remain in same relative order as the input, and the two sets of items will
+     * be combined to have the fewest elements moved from their locations in the original.
+     */
+    fun <T : Any> stabilizeTo(
+        sortedItems: Iterable<T>,
+        stableOrder: StableOrder<in T>,
+        output: MutableList<T>,
+    ): Boolean {
+        // Append to the output array if present
+        val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size)
+        sortedItems.filterTo(result) { stableOrder.getRank(it) != null }
+        val stableRankComparator = compareBy<T> { stableOrder.getRank(it)!! }
+        val isOrdered = result.isSorted(stableRankComparator)
+        if (!isOrdered) {
+            result.sortWith(stableRankComparator)
+        }
+        if (result.isEmpty()) {
+            sortedItems.filterTo(result) { stableOrder.getRank(it) == null }
+            return isOrdered
+        }
+        withAdditions<T> { additions ->
+            sortedItems.filterTo(additions) { stableOrder.getRank(it) == null }
+            if (additions.isNotEmpty()) {
+                withIndexOfComparator(sortedItems) { comparator ->
+                    insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator)
+                }
+            }
+        }
+        return isOrdered
+    }
+
+    private inline fun <T : Any, R> withWorkspace(block: (ArrayList<T>) -> R): R {
+        preallocatedWorkspace.clear()
+        val result = block(preallocatedWorkspace as ArrayList<T>)
+        preallocatedWorkspace.clear()
+        return result
+    }
+
+    private inline fun <T : Any> withAdditions(block: (ArrayList<T>) -> Unit) {
+        preallocatedAdditions.clear()
+        block(preallocatedAdditions as ArrayList<T>)
+        preallocatedAdditions.clear()
+    }
+
+    private inline fun <T : Any> withIndexOfComparator(
+        sortedItems: Iterable<T>,
+        block: (Comparator<in T>) -> Unit
+    ) {
+        preallocatedMapToIndex.clear()
+        sortedItems.forEachIndexed { i, item -> preallocatedMapToIndex[item] = i }
+        block(preallocatedMapToIndexComparator as Comparator<in T>)
+        preallocatedMapToIndex.clear()
+    }
+
+    companion object {
+
+        /**
+         * This is the core of the algorithm.
+         *
+         * Insert [preSortedAdditions] (the elements to be inserted) into [existing] without
+         * changing the relative order of any elements already in [existing], even though those
+         * elements may be mis-ordered relative to the [comparator], such that the total number of
+         * elements which are ordered incorrectly according to the [comparator] is fewest.
+         */
+        private fun <T> insertPreSortedElementsWithFewestMisOrderings(
+            existing: MutableList<T>,
+            preSortedAdditions: Iterable<T>,
+            comparator: Comparator<in T>,
+        ) {
+            if (DEBUG) println("  To $existing insert $preSortedAdditions with fewest misordering")
+            var iStart = 0
+            preSortedAdditions.forEach { toAdd ->
+                if (DEBUG) println("    need to add $toAdd to $existing, starting at $iStart")
+                var cmpSum = 0
+                var cmpSumMax = 0
+                var iCmpSumMax = iStart
+                if (DEBUG) print("      ")
+                for (i in iCmpSumMax until existing.size) {
+                    val cmp = comparator.compare(toAdd, existing[i]).sign
+                    cmpSum += cmp
+                    if (cmpSum > cmpSumMax) {
+                        cmpSumMax = cmpSum
+                        iCmpSumMax = i + 1
+                    }
+                    if (DEBUG) print("sum[$i]=$cmpSum, ")
+                }
+                if (DEBUG) println("inserting $toAdd at $iCmpSumMax")
+                existing.add(iCmpSumMax, toAdd)
+                iStart = iCmpSumMax + 1
+            }
+        }
+
+        /** Determines if a list is correctly sorted according to the given comparator */
+        @VisibleForTesting
+        fun <T> List<T>.isSorted(comparator: Comparator<T>): Boolean {
+            if (this.size <= 1) {
+                return true
+            }
+            val iterator = this.iterator()
+            var previous = iterator.next()
+            var current: T?
+            while (iterator.hasNext()) {
+                current = iterator.next()
+                if (comparator.compare(previous, current) > 0) {
+                    return false
+                }
+                previous = current
+            }
+            return true
+        }
+    }
+
+    fun interface StableOrder<T> {
+        fun getRank(item: T): Int?
+    }
+}
+
+val DEBUG = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt
new file mode 100644
index 0000000..d8f75f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.listbuilder
+
+import com.android.systemui.statusbar.notification.collection.ListEntry
+
+object ShadeListBuilderHelper {
+    fun getSectionSubLists(entries: List<ListEntry>): Iterable<List<ListEntry>> =
+        getContiguousSubLists(entries, minLength = 1) { it.sectionIndex }
+
+    inline fun <T : Any, K : Any> getContiguousSubLists(
+        itemList: List<T>,
+        minLength: Int = 1,
+        key: (T) -> K,
+    ): Iterable<List<T>> {
+        val subLists = mutableListOf<List<T>>()
+        val numEntries = itemList.size
+        var currentSectionStartIndex = 0
+        var currentSectionKey: K? = null
+        for (i in 0 until numEntries) {
+            val sectionKey = key(itemList[i])
+            if (currentSectionKey == null) {
+                currentSectionKey = sectionKey
+            } else if (currentSectionKey != sectionKey) {
+                val length = i - currentSectionStartIndex
+                if (length >= minLength) {
+                    subLists.add(itemList.subList(currentSectionStartIndex, i))
+                }
+                currentSectionStartIndex = i
+                currentSectionKey = sectionKey
+            }
+        }
+        val length = numEntries - currentSectionStartIndex
+        if (length >= minLength) {
+            subLists.add(itemList.subList(currentSectionStartIndex, numEntries))
+        }
+        return subLists
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
index d8dae5d..8e052c7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
@@ -16,11 +16,11 @@
 
 package com.android.systemui.statusbar.notification.collection.listbuilder
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.WARNING
 import com.android.systemui.statusbar.notification.NotifPipelineFlags
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.ListEntry
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
index aa27e1e..911a2d0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
@@ -20,13 +20,13 @@
 import android.service.notification.NotificationListenerService
 import android.service.notification.NotificationListenerService.RankingMap
 import android.service.notification.StatusBarNotification
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
-import com.android.systemui.log.LogLevel.WTF
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.WARNING
+import com.android.systemui.plugins.log.LogLevel.WTF
 import com.android.systemui.statusbar.notification.collection.NotifCollection
 import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason
 import com.android.systemui.statusbar.notification.collection.NotifCollection.FutureDismissal
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
index 38e3d49..9c71e5c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.collection.render
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.NotifPipelineFlags
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
 import com.android.systemui.util.Compile
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
index 6d1071c..b4b9438 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.collection.render
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import java.lang.RuntimeException
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
index 5dbec8d..d4f11fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
@@ -1,8 +1,8 @@
 package com.android.systemui.statusbar.notification.interruption
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
index 99d320d..073b6b0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
@@ -16,11 +16,11 @@
 
 package com.android.systemui.statusbar.notification.interruption
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
 import com.android.systemui.log.dagger.NotificationInterruptLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.WARNING
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
index fe03b2a..10197a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.logging
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationRenderLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import com.android.systemui.statusbar.notification.stack.NotificationSection
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
index ab91926..46fef3f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.row
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
index f9923b2..8a5d29a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.notification.row
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index 2719dd8..b2628e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -142,6 +142,11 @@
      */
     private boolean mIsFlingRequiredAfterLockScreenSwipeUp = false;
 
+    /**
+     * Whether the shade is currently closing.
+     */
+    private boolean mIsClosing;
+
     @VisibleForTesting
     public boolean isFlingRequiredAfterLockScreenSwipeUp() {
         return mIsFlingRequiredAfterLockScreenSwipeUp;
@@ -717,6 +722,20 @@
                 && mStatusBarKeyguardViewManager.isBouncerInTransit();
     }
 
+    /**
+     * @param isClosing Whether the shade is currently closing.
+     */
+    public void setIsClosing(boolean isClosing) {
+        mIsClosing = isClosing;
+    }
+
+    /**
+     * @return Whether the shade is currently closing.
+     */
+    public boolean isClosing() {
+        return mIsClosing;
+    }
+
     @Override
     public void dump(PrintWriter pw, String[] args) {
         pw.println("mTopPadding=" + mTopPadding);
@@ -761,5 +780,6 @@
                 + mIsFlingRequiredAfterLockScreenSwipeUp);
         pw.println("mZDistanceBetweenElements=" + mZDistanceBetweenElements);
         pw.println("mBaseZHeight=" + mBaseZHeight);
+        pw.println("mIsClosing=" + mIsClosing);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt
index cb7dfe8..b61c55e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsLogger.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.statusbar.notification.stack
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationSectionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import javax.inject.Inject
 
 private const val TAG = "NotifSections"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 55c577f..2272411 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -255,7 +255,6 @@
     private boolean mClearAllInProgress;
     private FooterClearAllListener mFooterClearAllListener;
     private boolean mFlingAfterUpEvent;
-
     /**
      * Was the scroller scrolled to the top when the down motion was observed?
      */
@@ -4020,8 +4019,9 @@
         setOwnScrollY(0);
     }
 
+    @VisibleForTesting
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
-    private void setIsExpanded(boolean isExpanded) {
+    void setIsExpanded(boolean isExpanded) {
         boolean changed = isExpanded != mIsExpanded;
         mIsExpanded = isExpanded;
         mStackScrollAlgorithm.setIsExpanded(isExpanded);
@@ -4842,13 +4842,21 @@
         }
     }
 
+    @VisibleForTesting
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
-    private void setOwnScrollY(int ownScrollY) {
+    void setOwnScrollY(int ownScrollY) {
         setOwnScrollY(ownScrollY, false /* animateScrollChangeListener */);
     }
 
     @ShadeViewRefactor(RefactorComponent.COORDINATOR)
     private void setOwnScrollY(int ownScrollY, boolean animateStackYChangeListener) {
+        // Avoid Flicking during clear all
+        // when the shade finishes closing, onExpansionStopped will call
+        // resetScrollPosition to setOwnScrollY to 0
+        if (mAmbientState.isClosing()) {
+            return;
+        }
+
         if (ownScrollY != mOwnScrollY) {
             // We still want to call the normal scrolled changed for accessibility reasons
             onScrollChanged(mScrollX, ownScrollY, mScrollX, mOwnScrollY);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
index 5f79c0e..4c52db7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
@@ -1,8 +1,8 @@
 package com.android.systemui.statusbar.notification.stack
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
index cb4a088..f5de678 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
@@ -1,8 +1,8 @@
 package com.android.systemui.statusbar.notification.stack
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
index 02b2354..4839fe6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
@@ -19,9 +19,9 @@
 import android.util.DisplayMetrics
 import android.view.View
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.LSShadeTransitionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
index b9a1413..81edff4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
@@ -17,12 +17,12 @@
 package com.android.systemui.statusbar.phone
 
 import android.app.PendingIntent
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogLevel.ERROR
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.WARNING
 import com.android.systemui.log.dagger.NotifInteractionLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogLevel.ERROR
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.WARNING
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
index 28ed080..d64bc58 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLogger.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.statusbar.phone.fragment
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.CollapsedSbFragmentLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
index dbb1aa5..d3cf32f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
@@ -18,10 +18,10 @@
 
 import android.net.Network
 import android.net.NetworkCapabilities
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.StatusBarConnectivityLog
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.toString
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
index 2f0ebf7..28a9b97 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
@@ -43,11 +43,7 @@
     }
 
     override fun getCount(): Int {
-        return if (controller.isKeyguardShowing) {
-            users.count { !it.isRestricted }
-        } else {
-            users.size
-        }
+        return users.size
     }
 
     override fun getItem(position: Int): UserRecord {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
index d7c81af..df1e80b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
@@ -16,10 +16,10 @@
 
 package com.android.systemui.statusbar.policy
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel.INFO
-import com.android.systemui.log.LogLevel.VERBOSE
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.INFO
+import com.android.systemui.plugins.log.LogLevel.VERBOSE
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
index 606a11a..a7185cb 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
@@ -16,8 +16,8 @@
 
 package com.android.systemui.temporarydisplay
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
 
 /** A logger for temporary view changes -- see [TemporaryViewDisplayController]. */
 open class TemporaryViewLogger(
diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt
index 51541bd..fda5114 100644
--- a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt
@@ -16,11 +16,11 @@
 
 package com.android.systemui.toast
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogLevel.DEBUG
-import com.android.systemui.log.LogMessage
 import com.android.systemui.log.dagger.ToastLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.plugins.log.LogMessage
 import javax.inject.Inject
 
 private const val TAG = "ToastLog"
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index 919e699..d768b6d 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -321,6 +321,7 @@
         return when {
             isAddUser -> false
             isAddSupervisedUser -> false
+            isManageUsers -> false
             isGuest -> info != null
             else -> true
         }
@@ -346,6 +347,7 @@
             isAddUser -> UserActionModel.ADD_USER
             isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER
             isGuest -> UserActionModel.ENTER_GUEST_MODE
+            isManageUsers -> UserActionModel.NAVIGATE_TO_USER_MANAGEMENT
             else -> error("Don't know how to convert to UserActionModel: $this")
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
index ba5a82a..0d5c64b 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
@@ -236,18 +236,7 @@
                     }
                     .flatMapLatest { isActionable ->
                         if (isActionable) {
-                            repository.actions.map { actions ->
-                                actions +
-                                    if (actions.isNotEmpty()) {
-                                        // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT
-                                        // because that's a user switcher specific action that is
-                                        // not known to the our data source or other features.
-                                        listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
-                                    } else {
-                                        // If no actions, don't add the navigate action.
-                                        emptyList()
-                                    }
-                            }
+                            repository.actions
                         } else {
                             // If not actionable it means that we're not allowed to show actions
                             // when
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
index 91c5921..f7e19c0 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.user.domain.interactor.UserInteractor
 import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.collect
@@ -41,19 +42,19 @@
 class UserSwitcherDialogCoordinator
 @Inject
 constructor(
-    @Application private val context: Context,
-    @Application private val applicationScope: CoroutineScope,
-    private val falsingManager: FalsingManager,
-    private val broadcastSender: BroadcastSender,
-    private val dialogLaunchAnimator: DialogLaunchAnimator,
-    private val interactor: UserInteractor,
-    private val featureFlags: FeatureFlags,
+    @Application private val context: Lazy<Context>,
+    @Application private val applicationScope: Lazy<CoroutineScope>,
+    private val falsingManager: Lazy<FalsingManager>,
+    private val broadcastSender: Lazy<BroadcastSender>,
+    private val dialogLaunchAnimator: Lazy<DialogLaunchAnimator>,
+    private val interactor: Lazy<UserInteractor>,
+    private val featureFlags: Lazy<FeatureFlags>,
 ) : CoreStartable {
 
     private var currentDialog: Dialog? = null
 
     override fun start() {
-        if (featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) {
+        if (featureFlags.get().isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) {
             return
         }
 
@@ -62,8 +63,8 @@
     }
 
     private fun startHandlingDialogShowRequests() {
-        applicationScope.launch {
-            interactor.dialogShowRequests.filterNotNull().collect { request ->
+        applicationScope.get().launch {
+            interactor.get().dialogShowRequests.filterNotNull().collect { request ->
                 currentDialog?.let {
                     if (it.isShowing) {
                         it.cancel()
@@ -74,48 +75,48 @@
                     when (request) {
                         is ShowDialogRequestModel.ShowAddUserDialog ->
                             AddUserDialog(
-                                context = context,
+                                context = context.get(),
                                 userHandle = request.userHandle,
                                 isKeyguardShowing = request.isKeyguardShowing,
                                 showEphemeralMessage = request.showEphemeralMessage,
-                                falsingManager = falsingManager,
-                                broadcastSender = broadcastSender,
-                                dialogLaunchAnimator = dialogLaunchAnimator,
+                                falsingManager = falsingManager.get(),
+                                broadcastSender = broadcastSender.get(),
+                                dialogLaunchAnimator = dialogLaunchAnimator.get(),
                             )
                         is ShowDialogRequestModel.ShowUserCreationDialog ->
                             UserCreatingDialog(
-                                context,
+                                context.get(),
                                 request.isGuest,
                             )
                         is ShowDialogRequestModel.ShowExitGuestDialog ->
                             ExitGuestDialog(
-                                context = context,
+                                context = context.get(),
                                 guestUserId = request.guestUserId,
                                 isGuestEphemeral = request.isGuestEphemeral,
                                 targetUserId = request.targetUserId,
                                 isKeyguardShowing = request.isKeyguardShowing,
-                                falsingManager = falsingManager,
-                                dialogLaunchAnimator = dialogLaunchAnimator,
+                                falsingManager = falsingManager.get(),
+                                dialogLaunchAnimator = dialogLaunchAnimator.get(),
                                 onExitGuestUserListener = request.onExitGuestUser,
                             )
                     }
 
                 currentDialog?.show()
-                interactor.onDialogShown()
+                interactor.get().onDialogShown()
             }
         }
     }
 
     private fun startHandlingDialogDismissRequests() {
-        applicationScope.launch {
-            interactor.dialogDismissRequests.filterNotNull().collect {
+        applicationScope.get().launch {
+            interactor.get().dialogDismissRequests.filterNotNull().collect {
                 currentDialog?.let {
                     if (it.isShowing) {
                         it.cancel()
                     }
                 }
 
-                interactor.onDialogDismissed()
+                interactor.get().onDialogDismissed()
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
index 219dae2..d857e85 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
@@ -62,17 +62,7 @@
     val isMenuVisible: Flow<Boolean> = _isMenuVisible
     /** The user action menu. */
     val menu: Flow<List<UserActionViewModel>> =
-        userInteractor.actions.map { actions ->
-            if (isNewImpl && actions.isNotEmpty()) {
-                    // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because that's a user
-                    // switcher specific action that is not known to the our data source or other
-                    // features.
-                    actions + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
-                } else {
-                    actions
-                }
-                .map { action -> toViewModel(action) }
-        }
+        userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } }
 
     /** Whether the button to open the user action menu is visible. */
     val isOpenMenuButtonVisible: Flow<Boolean> = menu.map { it.isNotEmpty() }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt
new file mode 100644
index 0000000..9d6aff2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2020 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.keyguard
+
+import android.content.Context
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.util.AttributeSet
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class BouncerKeyguardMessageAreaTest : SysuiTestCase() {
+    class FakeBouncerKeyguardMessageArea(context: Context, attrs: AttributeSet?) :
+        BouncerKeyguardMessageArea(context, attrs) {
+        override val SHOW_DURATION_MILLIS = 0L
+        override val HIDE_DURATION_MILLIS = 0L
+    }
+    lateinit var underTest: BouncerKeyguardMessageArea
+
+    @Before
+    fun setup() {
+        underTest = FakeBouncerKeyguardMessageArea(context, null)
+    }
+
+    @Test
+    fun testSetSameMessage() {
+        val underTestSpy = spy(underTest)
+        underTestSpy.setMessage("abc")
+        underTestSpy.setMessage("abc")
+        verify(underTestSpy, times(1)).text = "abc"
+    }
+
+    @Test
+    fun testSetDifferentMessage() {
+        underTest.setMessage("abc")
+        underTest.setMessage("def")
+        assertThat(underTest.text).isEqualTo("def")
+    }
+
+    @Test
+    fun testSetNullMessage() {
+        underTest.setMessage(null)
+        assertThat(underTest.text).isEqualTo("")
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 400caa3..9b2bba6 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -29,6 +29,7 @@
 
 import android.content.res.Resources;
 import android.database.ContentObserver;
+import android.graphics.Rect;
 import android.net.Uri;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -45,6 +46,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.plugins.ClockController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.clocks.AnimatableClockView;
@@ -262,6 +264,19 @@
         verify(mView).switchToClock(KeyguardClockSwitch.SMALL, /* animate */ true);
     }
 
+    @Test
+    public void testGetClockAnimationsForwardsToClock() {
+        ClockController mockClockController = mock(ClockController.class);
+        ClockAnimations mockClockAnimations = mock(ClockAnimations.class);
+        when(mClockEventController.getClock()).thenReturn(mockClockController);
+        when(mockClockController.getAnimations()).thenReturn(mockClockAnimations);
+
+        Rect r1 = new Rect(1, 2, 3, 4);
+        Rect r2 = new Rect(5, 6, 7, 8);
+        mController.getClockAnimations().onPositionUpdated(r1, r2, 0.2f);
+        verify(mockClockAnimations).onPositionUpdated(r1, r2, 0.2f);
+    }
+
     private void verifyAttachment(VerificationMode times) {
         verify(mClockRegistry, times).registerClockChangeListener(
                 any(ClockRegistry.ClockChangeListener.class));
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
index 69524e5..5d2b0ca 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java
@@ -17,13 +17,11 @@
 package com.android.keyguard;
 
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 
-import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -92,19 +90,4 @@
         mMessageAreaController.setIsVisible(true);
         verify(mKeyguardMessageArea).setIsVisible(true);
     }
-
-    @Test
-    public void testSetMessageIfEmpty_empty() {
-        mMessageAreaController.setMessage("");
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin);
-        verify(mKeyguardMessageArea).setMessage(R.string.keyguard_enter_your_pin);
-    }
-
-    @Test
-    public void testSetMessageIfEmpty_notEmpty() {
-        mMessageAreaController.setMessage("abc");
-        mMessageAreaController.setMessageIfEmpty(R.string.keyguard_enter_your_pin);
-        verify(mKeyguardMessageArea, never()).setMessage(getContext()
-                .getResources().getText(R.string.keyguard_enter_your_pin));
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
index b89dbd9..b369098 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
@@ -114,9 +114,8 @@
     }
 
     @Test
-    fun onResume_testSetInitialText() {
-        keyguardPasswordViewController.onResume(KeyguardSecurityView.SCREEN_ON)
-        verify(mKeyguardMessageAreaController)
-            .setMessageIfEmpty(R.string.keyguard_enter_your_password)
+    fun startAppearAnimation() {
+        keyguardPasswordViewController.startAppearAnimation()
+        verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index 3262a77..9eff704 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
@@ -100,16 +100,16 @@
     }
 
     @Test
-    fun onPause_clearsTextField() {
+    fun onPause_resetsText() {
         mKeyguardPatternViewController.init()
         mKeyguardPatternViewController.onPause()
-        verify(mKeyguardMessageAreaController).setMessage("")
+        verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern)
     }
 
+
     @Test
-    fun onResume_setInitialText() {
-        mKeyguardPatternViewController.onResume(KeyguardSecurityView.SCREEN_ON)
-        verify(mKeyguardMessageAreaController)
-            .setMessageIfEmpty(R.string.keyguard_enter_your_pattern)
+    fun startAppearAnimation() {
+        mKeyguardPatternViewController.startAppearAnimation()
+        verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
index 97d556b..ce1101f 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
@@ -113,11 +113,4 @@
         mKeyguardPinViewController.onResume(KeyguardSecurityView.SCREEN_ON);
         verify(mPasswordEntry).requestFocus();
     }
-
-    @Test
-    public void onResume_setInitialText() {
-        mKeyguardPinViewController.onResume(KeyguardSecurityView.SCREEN_ON);
-        verify(mKeyguardMessageAreaController).setMessageIfEmpty(R.string.keyguard_enter_your_pin);
-    }
 }
-
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index 9e5bfe5..d9efdea 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
@@ -98,6 +98,6 @@
     @Test
     fun startAppearAnimation() {
         pinViewController.startAppearAnimation()
-        verify(keyguardMessageAreaController).setMessageIfEmpty(R.string.keyguard_enter_your_pin)
+        verify(keyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
index 4dcaa7c..c94c97c 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
@@ -16,12 +16,16 @@
 
 package com.android.keyguard;
 
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
+import android.graphics.Rect;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -108,4 +112,16 @@
         configurationListenerArgumentCaptor.getValue().onLocaleListChanged();
         verify(mKeyguardClockSwitchController).onLocaleListChanged();
     }
+
+    @Test
+    public void getClockAnimations_forwardsToClockSwitch() {
+        ClockAnimations mockClockAnimations = mock(ClockAnimations.class);
+        when(mKeyguardClockSwitchController.getClockAnimations()).thenReturn(mockClockAnimations);
+
+        Rect r1 = new Rect(1, 2, 3, 4);
+        Rect r2 = new Rect(5, 6, 7, 8);
+        mController.getClockAnimations().onPositionUpdated(r1, r2, 0.3f);
+
+        verify(mockClockAnimations).onPositionUpdated(r1, r2, 0.3f);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index baeabc5..cd50144 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -163,6 +163,7 @@
         val sensorBounds = Rect(0, 0, SENSOR_WIDTH, SENSOR_HEIGHT)
         overlayParams = UdfpsOverlayParams(
             sensorBounds,
+            sensorBounds,
             DISPLAY_WIDTH,
             DISPLAY_HEIGHT,
             scaleFactor = 1f,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index f210708..eff47bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -414,7 +414,7 @@
         final float[] scaleFactor = new float[]{1f, displayHeight[1] / (float) displayHeight[0]};
         final int[] rotation = new int[]{Surface.ROTATION_0, Surface.ROTATION_90};
         final UdfpsOverlayParams oldParams = new UdfpsOverlayParams(sensorBounds[0],
-                displayWidth[0], displayHeight[0], scaleFactor[0], rotation[0]);
+                sensorBounds[0], displayWidth[0], displayHeight[0], scaleFactor[0], rotation[0]);
 
         for (int i1 = 0; i1 <= 1; ++i1) {
             for (int i2 = 0; i2 <= 1; ++i2) {
@@ -422,8 +422,8 @@
                     for (int i4 = 0; i4 <= 1; ++i4) {
                         for (int i5 = 0; i5 <= 1; ++i5) {
                             final UdfpsOverlayParams newParams = new UdfpsOverlayParams(
-                                    sensorBounds[i1], displayWidth[i2], displayHeight[i3],
-                                    scaleFactor[i4], rotation[i5]);
+                                    sensorBounds[i1], sensorBounds[i1], displayWidth[i2],
+                                    displayHeight[i3], scaleFactor[i4], rotation[i5]);
 
                             if (newParams.equals(oldParams)) {
                                 continue;
@@ -466,8 +466,8 @@
 
         // Initialize the overlay.
         mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        rotation));
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, rotation));
 
         // Show the overlay.
         mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, TEST_UDFPS_SENSOR_ID,
@@ -477,8 +477,8 @@
 
         // Update overlay with the same parameters.
         mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        rotation));
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, rotation));
         mFgExecutor.runAllReady();
 
         // Ensure the overlay was not recreated.
@@ -525,8 +525,8 @@
 
         // Test ROTATION_0
         mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        Surface.ROTATION_0));
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, Surface.ROTATION_0));
         MotionEvent event = obtainMotionEvent(ACTION_DOWN, displayWidth, displayHeight, touchMinor,
                 touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
@@ -542,8 +542,8 @@
         // Test ROTATION_90
         reset(mAlternateTouchProvider);
         mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        Surface.ROTATION_90));
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, Surface.ROTATION_90));
         event = obtainMotionEvent(ACTION_DOWN, displayHeight, 0, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
         mBiometricsExecutor.runAllReady();
@@ -558,8 +558,8 @@
         // Test ROTATION_270
         reset(mAlternateTouchProvider);
         mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        Surface.ROTATION_270));
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, Surface.ROTATION_270));
         event = obtainMotionEvent(ACTION_DOWN, 0, displayWidth, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
         mBiometricsExecutor.runAllReady();
@@ -574,8 +574,8 @@
         // Test ROTATION_180
         reset(mAlternateTouchProvider);
         mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID,
-                new UdfpsOverlayParams(sensorBounds, displayWidth, displayHeight, scaleFactor,
-                        Surface.ROTATION_180));
+                new UdfpsOverlayParams(sensorBounds, sensorBounds, displayWidth, displayHeight,
+                        scaleFactor, Surface.ROTATION_180));
         // ROTATION_180 is not supported. It should be treated like ROTATION_0.
         event = obtainMotionEvent(ACTION_DOWN, displayWidth, displayHeight, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
index b78c063..ac936e1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
@@ -68,7 +68,8 @@
         view = LayoutInflater.from(context).inflate(R.layout.udfps_view, null) as UdfpsView
         view.animationViewController = animationViewController
         val sensorBounds = SensorLocationInternal("", SENSOR_X, SENSOR_Y, SENSOR_RADIUS).rect
-        view.overlayParams = UdfpsOverlayParams(sensorBounds, 1920, 1080, 1f, Surface.ROTATION_0)
+        view.overlayParams = UdfpsOverlayParams(sensorBounds, sensorBounds, 1920,
+            1080, 1f, Surface.ROTATION_0)
         view.setUdfpsDisplayModeProvider(hbmProvider)
         ViewUtils.attachView(view)
     }
@@ -133,7 +134,8 @@
     @Test
     fun isNotWithinSensorArea() {
         whenever(animationViewController.touchTranslation).thenReturn(PointF(0f, 0f))
-        assertThat(view.isWithinSensorArea(SENSOR_RADIUS * 2.5f, SENSOR_RADIUS.toFloat())).isFalse()
+        assertThat(view.isWithinSensorArea(SENSOR_RADIUS * 2.5f, SENSOR_RADIUS.toFloat()))
+            .isFalse()
         assertThat(view.isWithinSensorArea(SENSOR_RADIUS.toFloat(), SENSOR_RADIUS * 2.5f)).isFalse()
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
index fc67201..65b44a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
@@ -17,11 +17,13 @@
 package com.android.systemui.dump
 
 import androidx.test.filters.SmallTest
+import com.android.systemui.CoreStartable
 import com.android.systemui.Dumpable
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager
 import com.android.systemui.util.mockito.any
+import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mock
@@ -30,6 +32,8 @@
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 import java.io.PrintWriter
+import java.io.StringWriter
+import javax.inject.Provider
 
 @SmallTest
 class DumpHandlerTest : SysuiTestCase() {
@@ -66,7 +70,9 @@
             mContext,
             dumpManager,
             logBufferEulogizer,
-            mutableMapOf(),
+            mutableMapOf(
+                EmptyCoreStartable::class.java to Provider { EmptyCoreStartable() }
+            ),
             exceptionHandlerManager
         )
     }
@@ -154,4 +160,20 @@
         verify(buffer1).dump(pw, 0)
         verify(buffer2).dump(pw, 0)
     }
-}
\ No newline at end of file
+
+    @Test
+    fun testConfigDump() {
+        // GIVEN a StringPrintWriter
+        val stringWriter = StringWriter()
+        val spw = PrintWriter(stringWriter)
+
+        // When a config dump is requested
+        dumpHandler.dump(spw, arrayOf("config"))
+
+        assertThat(stringWriter.toString()).contains(EmptyCoreStartable::class.java.simpleName)
+    }
+
+    private class EmptyCoreStartable : CoreStartable {
+        override fun start() {}
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
index bd029a7..64547f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.dump
 
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.plugins.log.LogcatEchoTracker
 
 /**
  * Creates a LogBuffer that will echo everything to logcat, which is useful for debugging tests.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
index 1078cda..e009e86 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt
@@ -19,9 +19,9 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogcatEchoTracker
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
 import java.io.StringWriter
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
index aacc695..68c10f2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentDisableFlagsLoggerTest.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogcatEchoTracker
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
index da52a9b..bc27bbc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.statusbar.policy.UserSwitcherController
 import com.android.systemui.user.data.source.UserRecord
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -139,6 +140,11 @@
         clickableTest(false, false, mUserDetailItemView, true)
     }
 
+    @Test
+    fun testManageUsersIsNotAvailable() {
+        assertNull(adapter.users.find { it.isManageUsers })
+    }
+
     private fun createUserRecord(current: Boolean, guest: Boolean) =
         UserRecord(
             UserInfo(0 /* id */, "name", 0 /* flags */),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
index 0151822..14a3bc1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
@@ -659,6 +659,51 @@
         verify(privacyIconsController, never()).onParentInvisible()
     }
 
+    @Test
+    fun clockPivotYInCenter() {
+        val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java)
+        verify(clock).addOnLayoutChangeListener(capture(captor))
+        var height = 100
+        val width = 50
+
+        clock.executeLayoutChange(0, 0, width, height, captor.value)
+        verify(clock).pivotY = height.toFloat() / 2
+
+        height = 150
+        clock.executeLayoutChange(0, 0, width, height, captor.value)
+        verify(clock).pivotY = height.toFloat() / 2
+    }
+
+    private fun View.executeLayoutChange(
+            left: Int,
+            top: Int,
+            right: Int,
+            bottom: Int,
+            listener: View.OnLayoutChangeListener
+    ) {
+        val oldLeft = this.left
+        val oldTop = this.top
+        val oldRight = this.right
+        val oldBottom = this.bottom
+        whenever(this.left).thenReturn(left)
+        whenever(this.top).thenReturn(top)
+        whenever(this.right).thenReturn(right)
+        whenever(this.bottom).thenReturn(bottom)
+        whenever(this.height).thenReturn(bottom - top)
+        whenever(this.width).thenReturn(right - left)
+        listener.onLayoutChange(
+                this,
+                oldLeft,
+                oldTop,
+                oldRight,
+                oldBottom,
+                left,
+                top,
+                right,
+                bottom
+        )
+    }
+
     private fun createWindowInsets(
         topCutout: Rect? = Rect()
     ): WindowInsets {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index c0dae03..ac02af87 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -38,6 +38,7 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -173,6 +174,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.stubbing.Answer;
@@ -1540,6 +1542,33 @@
         );
     }
 
+
+    /**
+     * When shade is flinging to close and this fling is not intercepted,
+     * {@link AmbientState#setIsClosing(boolean)} should be called before
+     * {@link NotificationStackScrollLayoutController#onExpansionStopped()}
+     * to ensure scrollY can be correctly set to be 0
+     */
+    @Test
+    public void onShadeFlingClosingEnd_mAmbientStateSetClose_thenOnExpansionStopped() {
+        // Given: Shade is expanded
+        mNotificationPanelViewController.notifyExpandingFinished();
+        mNotificationPanelViewController.setIsClosing(false);
+
+        // When: Shade flings to close not canceled
+        mNotificationPanelViewController.notifyExpandingStarted();
+        mNotificationPanelViewController.setIsClosing(true);
+        mNotificationPanelViewController.onFlingEnd(false);
+
+        // Then: AmbientState's mIsClosing should be set to false
+        // before mNotificationStackScrollLayoutController.onExpansionStopped() is called
+        // to ensure NotificationStackScrollLayout.resetScrollPosition() -> resetScrollPosition
+        // -> setOwnScrollY(0) can set scrollY to 0 when shade is closed
+        InOrder inOrder = inOrder(mAmbientState, mNotificationStackScrollLayoutController);
+        inOrder.verify(mAmbientState).setIsClosing(false);
+        inOrder.verify(mNotificationStackScrollLayoutController).onExpansionStopped();
+    }
+
     private static MotionEvent createMotionEvent(int x, int y, int action) {
         return MotionEvent.obtain(
                 /* downTime= */ 0, /* eventTime= */ 0, action, x, y, /* metaState= */ 0);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
index eb34561..cc45cf88 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.TextAnimator
+import com.android.systemui.util.mockito.any
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -55,7 +56,7 @@
         clockView.animateAppearOnLockscreen()
         clockView.measure(50, 50)
 
-        verify(mockTextAnimator).glyphFilter = null
+        verify(mockTextAnimator).glyphFilter = any()
         verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, false, 350L, null, 0L, null)
         verifyNoMoreInteractions(mockTextAnimator)
     }
@@ -66,7 +67,7 @@
         clockView.measure(50, 50)
         clockView.animateAppearOnLockscreen()
 
-        verify(mockTextAnimator, times(2)).glyphFilter = null
+        verify(mockTextAnimator, times(2)).glyphFilter = any()
         verify(mockTextAnimator).setTextStyle(100, -1.0f, 200, false, 0L, null, 0L, null)
         verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, true, 350L, null, 0L, null)
         verifyNoMoreInteractions(mockTextAnimator)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
index 5b34a95..b761647 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt
@@ -17,58 +17,58 @@
 
 @SmallTest
 class UncaughtExceptionPreHandlerTest : SysuiTestCase() {
-  private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager
+    private lateinit var preHandlerManager: UncaughtExceptionPreHandlerManager
 
-  @Mock private lateinit var mockHandler: UncaughtExceptionHandler
+    @Mock private lateinit var mockHandler: UncaughtExceptionHandler
 
-  @Mock private lateinit var mockHandler2: UncaughtExceptionHandler
+    @Mock private lateinit var mockHandler2: UncaughtExceptionHandler
 
-  @Before
-  fun setUp() {
-    MockitoAnnotations.initMocks(this)
-    Thread.setUncaughtExceptionPreHandler(null)
-    preHandlerManager = UncaughtExceptionPreHandlerManager()
-  }
-
-  @Test
-  fun registerHandler_registersOnceOnly() {
-    preHandlerManager.registerHandler(mockHandler)
-    preHandlerManager.registerHandler(mockHandler)
-    preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
-    verify(mockHandler, only()).uncaughtException(any(), any())
-  }
-
-  @Test
-  fun registerHandler_setsUncaughtExceptionPreHandler() {
-    Thread.setUncaughtExceptionPreHandler(null)
-    preHandlerManager.registerHandler(mockHandler)
-    assertThat(Thread.getUncaughtExceptionPreHandler()).isNotNull()
-  }
-
-  @Test
-  fun registerHandler_preservesOriginalHandler() {
-    Thread.setUncaughtExceptionPreHandler(mockHandler)
-    preHandlerManager.registerHandler(mockHandler2)
-    preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
-    verify(mockHandler, only()).uncaughtException(any(), any())
-  }
-
-  @Test
-  @Ignore
-  fun registerHandler_toleratesHandlersThatThrow() {
-    `when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException())
-    preHandlerManager.registerHandler(mockHandler2)
-    preHandlerManager.registerHandler(mockHandler)
-    preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
-    verify(mockHandler2, only()).uncaughtException(any(), any())
-    verify(mockHandler, only()).uncaughtException(any(), any())
-  }
-
-  @Test
-  fun registerHandler_doesNotSetUpTwice() {
-    UncaughtExceptionPreHandlerManager().registerHandler(mockHandler2)
-    assertThrows(IllegalStateException::class.java) {
-      preHandlerManager.registerHandler(mockHandler)
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        Thread.setUncaughtExceptionPreHandler(null)
+        preHandlerManager = UncaughtExceptionPreHandlerManager()
     }
-  }
+
+    @Test
+    fun registerHandler_registersOnceOnly() {
+        preHandlerManager.registerHandler(mockHandler)
+        preHandlerManager.registerHandler(mockHandler)
+        preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
+        verify(mockHandler, only()).uncaughtException(any(), any())
+    }
+
+    @Test
+    fun registerHandler_setsUncaughtExceptionPreHandler() {
+        Thread.setUncaughtExceptionPreHandler(null)
+        preHandlerManager.registerHandler(mockHandler)
+        assertThat(Thread.getUncaughtExceptionPreHandler()).isNotNull()
+    }
+
+    @Test
+    fun registerHandler_preservesOriginalHandler() {
+        Thread.setUncaughtExceptionPreHandler(mockHandler)
+        preHandlerManager.registerHandler(mockHandler2)
+        preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
+        verify(mockHandler, only()).uncaughtException(any(), any())
+    }
+
+    @Test
+    @Ignore
+    fun registerHandler_toleratesHandlersThatThrow() {
+        `when`(mockHandler2.uncaughtException(any(), any())).thenThrow(RuntimeException())
+        preHandlerManager.registerHandler(mockHandler2)
+        preHandlerManager.registerHandler(mockHandler)
+        preHandlerManager.handleUncaughtException(Thread.currentThread(), Exception())
+        verify(mockHandler2, only()).uncaughtException(any(), any())
+        verify(mockHandler, only()).uncaughtException(any(), any())
+    }
+
+    @Test
+    fun registerHandler_doesNotSetUpTwice() {
+        UncaughtExceptionPreHandlerManager().registerHandler(mockHandler2)
+        assertThrows(IllegalStateException::class.java) {
+            preHandlerManager.registerHandler(mockHandler)
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
index 8cb530c..5fc0ffe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
@@ -4,7 +4,7 @@
 import android.util.DisplayMetrics
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.log.LogBuffer
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import com.android.systemui.statusbar.phone.LSShadeTransitionLogger
 import com.android.systemui.statusbar.phone.LockscreenGestureLogger
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
index f8a0d2f..9c65fac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerBaseTest.java
@@ -70,7 +70,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
 import com.android.systemui.telephony.TelephonyListenerManager;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
index ed8a3e1..4bed4a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerDataTest.java
@@ -38,7 +38,7 @@
 import com.android.settingslib.mobile.TelephonyIcons;
 import com.android.settingslib.net.DataUsageController;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.util.CarrierConfigTracker;
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
index a76676e..d5f5105 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerSignalTest.java
@@ -43,7 +43,7 @@
 import com.android.settingslib.net.DataUsageController;
 import com.android.systemui.R;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.log.LogBuffer;
+import com.android.systemui.plugins.log.LogBuffer;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.util.CarrierConfigTracker;
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
index 82e32b2..09f8a10 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
@@ -34,10 +34,12 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import static java.util.Arrays.asList;
 import static java.util.Collections.singletonList;
@@ -135,6 +137,7 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         allowTestableLooperAsMainThread();
+        when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true);
 
         mListBuilder = new ShadeListBuilder(
                 mDumpManager,
@@ -1995,22 +1998,89 @@
     }
 
     @Test
-    public void testStableOrdering() {
+    public void testActiveOrdering_withLegacyStability() {
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false);
+        assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change
+        assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X
+        assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change
+        assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X
+        assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap
+    }
+
+    @Test
+    public void testStableOrdering_withLegacyStability() {
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false);
         mStabilityManager.setAllowEntryReordering(false);
-        assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG"); // X
-        assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG"); // no change
-        assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG"); // Z and X
-        assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG"); // Z and X + gap
-        verify(mStabilityManager, times(4)).onEntryReorderSuppressed();
+        assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change
+        assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG", false); // X
+        assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false); // no change
+        assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG", false); // Z and X
+        assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG", false); // Z and X + gap
+    }
+
+    @Test
+    public void testStableOrdering() {
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
+        mStabilityManager.setAllowEntryReordering(false);
+        // No input or output
+        assertOrder("", "", "", true);
+        // Remove everything
+        assertOrder("ABCDEFG", "", "", true);
+        // Literally no changes
+        assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true);
+
+        // No stable order
+        assertOrder("", "ABCDEFG", "ABCDEFG", true);
+
+        // F moved after A, and...
+        assertOrder("ABCDEFG", "AFBCDEG", "ABCDEFG", false);   // No other changes
+        assertOrder("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false); // Insert X before F
+        assertOrder("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false); // Insert X after F
+        assertOrder("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false); // Insert X where F was
+
+        // B moved after F, and...
+        assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false);   // No other changes
+        assertOrder("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false); // Insert X before B
+        assertOrder("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false); // Insert X after B
+        assertOrder("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false); // Insert X where B was
+
+        // Swap F and B, and...
+        assertOrder("ABCDEFG", "AFCDEBG", "ABCDEFG", false);   // No other changes
+        assertOrder("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false); // Insert X before F
+        assertOrder("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false); // Insert X after F
+        assertOrder("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false); // Insert X between CD (or: ABCXDEFG)
+        assertOrder("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false); // Insert X between DE (or: ABCDEFXG)
+        assertOrder("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false); // Insert X before B
+        assertOrder("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false); // Insert X after B
+
+        // Remove a bunch of entries at once
+        assertOrder("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true);
+
+        // Remove a bunch of entries and scramble
+        assertOrder("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false);
+
+        // Add a bunch of entries at once
+        assertOrder("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true);
+
+        // Add a bunch of entries and reverse originals
+        // NOTE: Some of these don't have obviously correct answers
+        assertOrder("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false); // appended
+        assertOrder("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false); // prepended
+        assertOrder("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false); // closer to back: append
+        assertOrder("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false); // closer to front: prepend
+        assertOrder("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false); // split new entries
+
+        // Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout
+        assertOrder("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false);
     }
 
     @Test
     public void testActiveOrdering() {
-        assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG"); // X
-        assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG"); // no change
-        assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG"); // Z and X
-        assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG"); // Z and X + gap
-        verify(mStabilityManager, never()).onEntryReorderSuppressed();
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
+        assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X
+        assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change
+        assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X
+        assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap
     }
 
     @Test
@@ -2062,6 +2132,52 @@
     }
 
     @Test
+    public void stableOrderingDisregardedWithSectionChange() {
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
+        // GIVEN the first sectioner's packages can be changed from run-to-run
+        List<String> mutableSectionerPackages = new ArrayList<>();
+        mutableSectionerPackages.add(PACKAGE_1);
+        mListBuilder.setSectioners(asList(
+                new PackageSectioner(mutableSectionerPackages, null),
+                new PackageSectioner(List.of(PACKAGE_1, PACKAGE_2, PACKAGE_3), null)));
+        mStabilityManager.setAllowEntryReordering(false);
+
+        // WHEN the list is originally built with reordering disabled (and section changes allowed)
+        addNotif(0, PACKAGE_1).setRank(4);
+        addNotif(1, PACKAGE_1).setRank(5);
+        addNotif(2, PACKAGE_2).setRank(1);
+        addNotif(3, PACKAGE_2).setRank(2);
+        addNotif(4, PACKAGE_3).setRank(3);
+        dispatchBuild();
+
+        // VERIFY the order and that entry reordering has not been suppressed
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                notif(2),
+                notif(3),
+                notif(4)
+        );
+        verify(mStabilityManager, never()).onEntryReorderSuppressed();
+
+        // WHEN the first section now claims PACKAGE_3 notifications
+        mutableSectionerPackages.add(PACKAGE_3);
+        dispatchBuild();
+
+        // VERIFY the re-sectioned notification is inserted at #1 of the first section, which
+        // is the correct position based on its rank, rather than #3 in the new section simply
+        // because it was #3 in its previous section.
+        verifyBuiltList(
+                notif(4),
+                notif(0),
+                notif(1),
+                notif(2),
+                notif(3)
+        );
+        verify(mStabilityManager, never()).onEntryReorderSuppressed();
+    }
+
+    @Test
     public void testStableChildOrdering() {
         // WHEN the list is originally built with reordering disabled
         mStabilityManager.setAllowEntryReordering(false);
@@ -2112,6 +2228,85 @@
         );
     }
 
+    @Test
+    public void groupRevertingToSummaryDoesNotRetainStablePositionWithLegacyIndexLogic() {
+        when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(false);
+
+        // GIVEN a notification group is on screen
+        mStabilityManager.setAllowEntryReordering(false);
+
+        // WHEN the list is originally built with reordering disabled (and section changes allowed)
+        addNotif(0, PACKAGE_1).setRank(2);
+        addNotif(1, PACKAGE_1).setRank(3);
+        addGroupSummary(2, PACKAGE_1, "group").setRank(4);
+        addGroupChild(3, PACKAGE_1, "group").setRank(5);
+        addGroupChild(4, PACKAGE_1, "group").setRank(6);
+        dispatchBuild();
+
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                group(
+                        summary(2),
+                        child(3),
+                        child(4)
+                )
+        );
+
+        // WHEN the notification summary rank increases and children removed
+        setNewRank(notif(2).entry, 1);
+        mEntrySet.remove(4);
+        mEntrySet.remove(3);
+        dispatchBuild();
+
+        // VERIFY the summary (incorrectly) moves to the top of the section where it is ranked,
+        // despite visual stability being active
+        verifyBuiltList(
+                notif(2),
+                notif(0),
+                notif(1)
+        );
+    }
+
+    @Test
+    public void groupRevertingToSummaryRetainsStablePosition() {
+        when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true);
+
+        // GIVEN a notification group is on screen
+        mStabilityManager.setAllowEntryReordering(false);
+
+        // WHEN the list is originally built with reordering disabled (and section changes allowed)
+        addNotif(0, PACKAGE_1).setRank(2);
+        addNotif(1, PACKAGE_1).setRank(3);
+        addGroupSummary(2, PACKAGE_1, "group").setRank(4);
+        addGroupChild(3, PACKAGE_1, "group").setRank(5);
+        addGroupChild(4, PACKAGE_1, "group").setRank(6);
+        dispatchBuild();
+
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                group(
+                        summary(2),
+                        child(3),
+                        child(4)
+                )
+        );
+
+        // WHEN the notification summary rank increases and children removed
+        setNewRank(notif(2).entry, 1);
+        mEntrySet.remove(4);
+        mEntrySet.remove(3);
+        dispatchBuild();
+
+        // VERIFY the summary stays in the same location on rebuild
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                notif(2)
+        );
+    }
+
     private static void setNewRank(NotificationEntry entry, int rank) {
         entry.setRanking(new RankingBuilder(entry.getRanking()).setRank(rank).build());
     }
@@ -2255,26 +2450,35 @@
         return addGroupChildWithTag(index, packageId, groupId, null);
     }
 
-    private void assertOrder(String visible, String active, String expected) {
+    private void assertOrder(String visible, String active, String expected,
+            boolean isOrderedCorrectly) {
         StringBuilder differenceSb = new StringBuilder();
+        NotifSection section = new NotifSection(mock(NotifSectioner.class), 0);
         for (char c : active.toCharArray()) {
             if (visible.indexOf(c) < 0) differenceSb.append(c);
         }
         String difference = differenceSb.toString();
 
+        int globalIndex = 0;
         for (int i = 0; i < visible.length(); i++) {
-            addNotif(i, String.valueOf(visible.charAt(i)))
-                    .setRank(active.indexOf(visible.charAt(i)))
+            final char c = visible.charAt(i);
+            // Skip notifications which aren't active anymore
+            if (!active.contains(String.valueOf(c))) continue;
+            addNotif(globalIndex++, String.valueOf(c))
+                    .setRank(active.indexOf(c))
+                    .setSection(section)
                     .setStableIndex(i);
-
         }
 
-        for (int i = 0; i < difference.length(); i++) {
-            addNotif(i + visible.length(), String.valueOf(difference.charAt(i)))
-                    .setRank(active.indexOf(difference.charAt(i)))
+        for (char c : difference.toCharArray()) {
+            addNotif(globalIndex++, String.valueOf(c))
+                    .setRank(active.indexOf(c))
+                    .setSection(section)
                     .setStableIndex(-1);
         }
 
+        clearInvocations(mStabilityManager);
+
         dispatchBuild();
         StringBuilder resultSb = new StringBuilder();
         for (int i = 0; i < expected.length(); i++) {
@@ -2284,6 +2488,9 @@
         assertEquals("visible [" + visible + "] active [" + active + "]",
                 expected, resultSb.toString());
         mEntrySet.clear();
+
+        verify(mStabilityManager, isOrderedCorrectly ? never() : times(1))
+                .onEntryReorderSuppressed();
     }
 
     private int nextId(String packageName) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
index dcf2455..f4adf69 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
@@ -181,7 +181,7 @@
     @Test
     public void testInflatesNewNotification() {
         // WHEN there is a new notification
-        mCollectionListener.onEntryInit(mEntry);
+        mCollectionListener.onEntryAdded(mEntry);
         mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
 
         // THEN we inflate it
@@ -194,7 +194,7 @@
     @Test
     public void testRebindsInflatedNotificationsOnUpdate() {
         // GIVEN an inflated notification
-        mCollectionListener.onEntryInit(mEntry);
+        mCollectionListener.onEntryAdded(mEntry);
         mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
         verify(mNotifInflater).inflateViews(eq(mEntry), any(), any());
         mNotifInflater.invokeInflateCallbackForEntry(mEntry);
@@ -213,7 +213,7 @@
     @Test
     public void testEntrySmartReplyAdditionWillRebindViews() {
         // GIVEN an inflated notification
-        mCollectionListener.onEntryInit(mEntry);
+        mCollectionListener.onEntryAdded(mEntry);
         mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
         verify(mNotifInflater).inflateViews(eq(mEntry), any(), any());
         mNotifInflater.invokeInflateCallbackForEntry(mEntry);
@@ -232,7 +232,7 @@
     @Test
     public void testEntryChangedToMinimizedSectionWillRebindViews() {
         // GIVEN an inflated notification
-        mCollectionListener.onEntryInit(mEntry);
+        mCollectionListener.onEntryAdded(mEntry);
         mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
         verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any());
         assertFalse(mParamsCaptor.getValue().isLowPriority());
@@ -254,36 +254,28 @@
     public void testMinimizedEntryMovedIntoGroupWillRebindViews() {
         // GIVEN an inflated, minimized notification
         setSectionIsLowPriority(true);
-        mCollectionListener.onEntryInit(mEntry);
+        mCollectionListener.onEntryAdded(mEntry);
         mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
         verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any());
         assertTrue(mParamsCaptor.getValue().isLowPriority());
         mNotifInflater.invokeInflateCallbackForEntry(mEntry);
 
         // WHEN notification is moved under a parent
-        NotificationEntry groupSummary = getNotificationEntryBuilder()
-                .setParent(ROOT_ENTRY)
-                .setGroupSummary(mContext, true)
-                .setGroup(mContext, TEST_GROUP_KEY)
-                .build();
-        GroupEntry parent = mock(GroupEntry.class);
-        when(parent.getSummary()).thenReturn(groupSummary);
-        NotificationEntryBuilder.setNewParent(mEntry, parent);
-        mCollectionListener.onEntryInit(groupSummary);
-        mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry, groupSummary));
+        NotificationEntryBuilder.setNewParent(mEntry, mock(GroupEntry.class));
+        mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
 
         // THEN we rebind it as not-minimized
         verify(mNotifInflater).rebindViews(eq(mEntry), mParamsCaptor.capture(), any());
         assertFalse(mParamsCaptor.getValue().isLowPriority());
 
-        // THEN we filter it because the parent summary is not yet inflated.
-        assertTrue(mUninflatedFilter.shouldFilterOut(mEntry, 0));
+        // THEN we do not filter it because it's not the first inflation.
+        assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0));
     }
 
     @Test
     public void testEntryRankChangeWillNotRebindViews() {
         // GIVEN an inflated notification
-        mCollectionListener.onEntryInit(mEntry);
+        mCollectionListener.onEntryAdded(mEntry);
         mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
         verify(mNotifInflater).inflateViews(eq(mEntry), any(), any());
         mNotifInflater.invokeInflateCallbackForEntry(mEntry);
@@ -302,7 +294,7 @@
     @Test
     public void testDoesntFilterInflatedNotifs() {
         // GIVEN an inflated notification
-        mCollectionListener.onEntryInit(mEntry);
+        mCollectionListener.onEntryAdded(mEntry);
         mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
         verify(mNotifInflater).inflateViews(eq(mEntry), any(), any());
         mNotifInflater.invokeInflateCallbackForEntry(mEntry);
@@ -338,9 +330,9 @@
             mCollectionListener.onEntryInit(entry);
         }
 
-        mCollectionListener.onEntryInit(summary);
+        mCollectionListener.onEntryAdded(summary);
         for (NotificationEntry entry : children) {
-            mCollectionListener.onEntryInit(entry);
+            mCollectionListener.onEntryAdded(entry);
         }
 
         mBeforeFilterListener.onBeforeFinalizeFilter(List.of(groupEntry));
@@ -401,40 +393,6 @@
     }
 
     @Test
-    public void testPartiallyInflatedGroupsAreNotFilteredOutIfSummaryReinflate() {
-        // GIVEN a newly-posted group with a summary and two children
-        final String groupKey = "test_reinflate_group";
-        final int summaryId = 1;
-        final GroupEntry group = new GroupEntryBuilder()
-                .setKey(groupKey)
-                .setCreationTime(400)
-                .setSummary(getNotificationEntryBuilder().setId(summaryId).setImportance(1).build())
-                .addChild(getNotificationEntryBuilder().setId(2).build())
-                .addChild(getNotificationEntryBuilder().setId(3).build())
-                .build();
-        fireAddEvents(List.of(group));
-        final NotificationEntry summary = group.getSummary();
-        final NotificationEntry child0 = group.getChildren().get(0);
-        final NotificationEntry child1 = group.getChildren().get(1);
-        mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group));
-
-        // WHEN all of the children (but not the summary) finish inflating
-        mNotifInflater.invokeInflateCallbackForEntry(child0);
-        mNotifInflater.invokeInflateCallbackForEntry(child1);
-        mNotifInflater.invokeInflateCallbackForEntry(summary);
-
-        // WHEN the summary is updated and starts re-inflating
-        summary.setRanking(new RankingBuilder(summary.getRanking()).setImportance(4).build());
-        fireUpdateEvents(summary);
-        mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group));
-
-        // THEN the entire group is still not filtered out
-        assertFalse(mUninflatedFilter.shouldFilterOut(summary, 401));
-        assertFalse(mUninflatedFilter.shouldFilterOut(child0, 401));
-        assertFalse(mUninflatedFilter.shouldFilterOut(child1, 401));
-    }
-
-    @Test
     public void testCompletedInflatedGroupsAreReleased() {
         // GIVEN a newly-posted group with a summary and two children
         final GroupEntry group = new GroupEntryBuilder()
@@ -454,7 +412,7 @@
         mNotifInflater.invokeInflateCallbackForEntry(child1);
         mNotifInflater.invokeInflateCallbackForEntry(summary);
 
-        // THEN the entire group is no longer filtered out
+        // THEN the entire group is still filtered out
         assertFalse(mUninflatedFilter.shouldFilterOut(summary, 401));
         assertFalse(mUninflatedFilter.shouldFilterOut(child0, 401));
         assertFalse(mUninflatedFilter.shouldFilterOut(child1, 401));
@@ -536,11 +494,7 @@
 
     private void fireAddEvents(NotificationEntry entry) {
         mCollectionListener.onEntryInit(entry);
-        mCollectionListener.onEntryInit(entry);
-    }
-
-    private void fireUpdateEvents(NotificationEntry entry) {
-        mCollectionListener.onEntryUpdated(entry);
+        mCollectionListener.onEntryAdded(entry);
     }
 
     private static final String TEST_MESSAGE = "TEST_MESSAGE";
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt
new file mode 100644
index 0000000..1cdd023
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.collection.listbuilder
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.util.Log
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class SemiStableSortTest : SysuiTestCase() {
+
+    var shuffleInput: Boolean = false
+    var testStabilizeTo: Boolean = false
+    var sorter: SemiStableSort? = null
+
+    @Before
+    fun setUp() {
+        shuffleInput = false
+        sorter = null
+    }
+
+    private fun stringStabilizeTo(
+        stableOrder: String,
+        activeOrder: String,
+    ): Pair<String, Boolean> {
+        val actives = activeOrder.toMutableList()
+        val result = mutableListOf<Char>()
+        return (sorter ?: SemiStableSort())
+            .stabilizeTo(
+                actives,
+                { ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } },
+                result,
+            )
+            .let { ordered -> result.joinToString("") to ordered }
+    }
+
+    private fun stringSort(
+        stableOrder: String,
+        activeOrder: String,
+    ): Pair<String, Boolean> {
+        val actives = activeOrder.toMutableList()
+        if (shuffleInput) {
+            actives.shuffle()
+        }
+        return (sorter ?: SemiStableSort())
+            .sort(
+                actives,
+                { ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } },
+                compareBy { activeOrder.indexOf(it) },
+            )
+            .let { ordered -> actives.joinToString("") to ordered }
+    }
+
+    private fun testCase(
+        stableOrder: String,
+        activeOrder: String,
+        expected: String,
+        expectOrdered: Boolean,
+    ) {
+        val (mergeResult, ordered) =
+            if (testStabilizeTo) stringStabilizeTo(stableOrder, activeOrder)
+            else stringSort(stableOrder, activeOrder)
+        val resultPass = expected == mergeResult
+        val orderedPass = ordered == expectOrdered
+        val pass = resultPass && orderedPass
+        val resultSuffix =
+            if (resultPass) "result=$expected" else "expected=$expected got=$mergeResult"
+        val orderedSuffix =
+            if (orderedPass) "ordered=$ordered" else "expected ordered to be $expectOrdered"
+        val readableResult = "stable=$stableOrder active=$activeOrder $resultSuffix $orderedSuffix"
+        Log.d("SemiStableSortTest", "${if (pass) "PASS" else "FAIL"}: $readableResult")
+        if (!pass) {
+            throw AssertionError("Test case failed: $readableResult")
+        }
+    }
+
+    private fun runAllTestCases() {
+        // No input or output
+        testCase("", "", "", true)
+        // Remove everything
+        testCase("ABCDEFG", "", "", true)
+        // Literally no changes
+        testCase("ABCDEFG", "ABCDEFG", "ABCDEFG", true)
+
+        // No stable order
+        testCase("", "ABCDEFG", "ABCDEFG", true)
+
+        // F moved after A, and...
+        testCase("ABCDEFG", "AFBCDEG", "ABCDEFG", false) // No other changes
+        testCase("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false) // Insert X before F
+        testCase("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false) // Insert X after F
+        testCase("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false) // Insert X where F was
+
+        // B moved after F, and...
+        testCase("ABCDEFG", "ACDEFBG", "ABCDEFG", false) // No other changes
+        testCase("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false) // Insert X before B
+        testCase("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false) // Insert X after B
+        testCase("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false) // Insert X where B was
+
+        // Swap F and B, and...
+        testCase("ABCDEFG", "AFCDEBG", "ABCDEFG", false) // No other changes
+        testCase("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false) // Insert X before F
+        testCase("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false) // Insert X after F
+        testCase("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false) // Insert X between CD (Alt: ABCXDEFG)
+        testCase("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false) // Insert X between DE (Alt: ABCDEFXG)
+        testCase("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false) // Insert X before B
+        testCase("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false) // Insert X after B
+
+        // Remove a bunch of entries at once
+        testCase("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true)
+
+        // Remove a bunch of entries and scramble
+        testCase("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false)
+
+        // Add a bunch of entries at once
+        testCase("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true)
+
+        // Add a bunch of entries and reverse originals
+        // NOTE: Some of these don't have obviously correct answers
+        testCase("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false) // appended
+        testCase("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false) // prepended
+        testCase("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false) // closer to back: append
+        testCase("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false) // closer to front: prepend
+        testCase("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false) // split new entries
+
+        // Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout
+        testCase("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false)
+    }
+
+    @Test
+    fun testSort() {
+        testStabilizeTo = false
+        shuffleInput = false
+        sorter = null
+        runAllTestCases()
+    }
+
+    @Test
+    fun testSortWithSingleInstance() {
+        testStabilizeTo = false
+        shuffleInput = false
+        sorter = SemiStableSort()
+        runAllTestCases()
+    }
+
+    @Test
+    fun testSortWithShuffledInput() {
+        testStabilizeTo = false
+        shuffleInput = true
+        sorter = null
+        runAllTestCases()
+    }
+
+    @Test
+    fun testStabilizeTo() {
+        testStabilizeTo = true
+        sorter = null
+        runAllTestCases()
+    }
+
+    @Test
+    fun testStabilizeToWithSingleInstance() {
+        testStabilizeTo = true
+        sorter = SemiStableSort()
+        runAllTestCases()
+    }
+
+    @Test
+    fun testIsSorted() {
+        val intCmp = Comparator<Int> { x, y -> Integer.compare(x, y) }
+        SemiStableSort.apply {
+            assertTrue(emptyList<Int>().isSorted(intCmp))
+            assertTrue(listOf(1).isSorted(intCmp))
+            assertTrue(listOf(1, 2).isSorted(intCmp))
+            assertTrue(listOf(1, 2, 3).isSorted(intCmp))
+            assertTrue(listOf(1, 2, 3, 4).isSorted(intCmp))
+            assertTrue(listOf(1, 2, 3, 4, 5).isSorted(intCmp))
+            assertTrue(listOf(1, 1, 1, 1, 1).isSorted(intCmp))
+            assertTrue(listOf(1, 1, 2, 2, 3, 3).isSorted(intCmp))
+            assertFalse(listOf(2, 1).isSorted(intCmp))
+            assertFalse(listOf(2, 1, 2).isSorted(intCmp))
+            assertFalse(listOf(1, 2, 1).isSorted(intCmp))
+            assertFalse(listOf(1, 2, 3, 2, 5).isSorted(intCmp))
+            assertFalse(listOf(5, 2, 3, 4, 5).isSorted(intCmp))
+            assertFalse(listOf(1, 2, 3, 4, 1).isSorted(intCmp))
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt
new file mode 100644
index 0000000..2036954
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.collection.listbuilder
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper.getContiguousSubLists
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class ShadeListBuilderHelperTest : SysuiTestCase() {
+
+    @Test
+    fun testGetContiguousSubLists() {
+        assertThat(getContiguousSubLists("AAAAAA".toList()) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A', 'A', 'A', 'A'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABBB".toList()) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('B', 'B', 'B'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABAA".toList()) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('B'),
+                listOf('A', 'A'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABAA".toList(), minLength = 2) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('A', 'A'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList()) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('B', 'B', 'B', 'B'),
+                listOf('C', 'C'),
+                listOf('D'),
+                listOf('E', 'E', 'E'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList(), minLength = 2) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('B', 'B', 'B', 'B'),
+                listOf('C', 'C'),
+                listOf('E', 'E', 'E'),
+            )
+            .inOrder()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
index 11798a7..87f4c32 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
@@ -361,6 +361,22 @@
         assertThat(sut.isOnKeyguard).isFalse()
     }
     // endregion
+
+    // region mIsClosing
+    @Test
+    fun isClosing_whenShadeClosing_shouldReturnTrue() {
+        sut.setIsClosing(true)
+
+        assertThat(sut.isClosing).isTrue()
+    }
+
+    @Test
+    fun isClosing_whenShadeFinishClosing_shouldReturnFalse() {
+        sut.setIsClosing(false)
+
+        assertThat(sut.isClosing).isFalse()
+    }
+    // endregion
 }
 
 // region Arrange helper methods.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 4353036..35c8b61 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -728,6 +728,57 @@
         verify(mNotificationStackSizeCalculator).computeHeight(any(), anyInt(), anyFloat());
     }
 
+    @Test
+    public void testSetOwnScrollY_shadeNotClosing_scrollYChanges() {
+        // Given: shade is not closing, scrollY is 0
+        mAmbientState.setScrollY(0);
+        assertEquals(0, mAmbientState.getScrollY());
+        mAmbientState.setIsClosing(false);
+
+        // When: call NotificationStackScrollLayout.setOwnScrollY to set scrollY to 1
+        mStackScroller.setOwnScrollY(1);
+
+        // Then: scrollY should be set to 1
+        assertEquals(1, mAmbientState.getScrollY());
+
+        // Reset scrollY back to 0 to avoid interfering with other tests
+        mStackScroller.setOwnScrollY(0);
+        assertEquals(0, mAmbientState.getScrollY());
+    }
+
+    @Test
+    public void testSetOwnScrollY_shadeClosing_scrollYDoesNotChange() {
+        // Given: shade is closing, scrollY is 0
+        mAmbientState.setScrollY(0);
+        assertEquals(0, mAmbientState.getScrollY());
+        mAmbientState.setIsClosing(true);
+
+        // When: call NotificationStackScrollLayout.setOwnScrollY to set scrollY to 1
+        mStackScroller.setOwnScrollY(1);
+
+        // Then: scrollY should not change, it should still be 0
+        assertEquals(0, mAmbientState.getScrollY());
+
+        // Reset scrollY and mAmbientState.mIsClosing to avoid interfering with other tests
+        mAmbientState.setIsClosing(false);
+        mStackScroller.setOwnScrollY(0);
+        assertEquals(0, mAmbientState.getScrollY());
+    }
+
+    @Test
+    public void onShadeFlingClosingEnd_scrollYShouldBeSetToZero() {
+        // Given: mAmbientState.mIsClosing is set to be true
+        // mIsExpanded is set to be false
+        mAmbientState.setIsClosing(true);
+        mStackScroller.setIsExpanded(false);
+
+        // When: onExpansionStopped is called
+        mStackScroller.onExpansionStopped();
+
+        // Then: mAmbientState.scrollY should be set to be 0
+        assertEquals(mAmbientState.getScrollY(), 0);
+    }
+
     private void setBarStateForTest(int state) {
         // Can't inject this through the listener or we end up on the actual implementation
         // rather than the mock because the spy just coppied the anonymous inner /shruggie.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
index 65e2964..3a0a94d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentLoggerTest.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogcatEchoTracker
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 3a006ad..36e76f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -49,9 +49,9 @@
 import com.android.systemui.SysuiBaseFragmentTest;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.log.LogBuffer;
-import com.android.systemui.log.LogcatEchoTracker;
 import com.android.systemui.plugins.DarkIconDispatcher;
+import com.android.systemui.plugins.log.LogBuffer;
+import com.android.systemui.plugins.log.LogcatEchoTracker;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.ShadeExpansionStateManager;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
index 0e75c74..b32058f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
@@ -22,7 +22,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogcatEchoTracker
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
 import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
index c9f2b4d..13e9f60 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
@@ -19,9 +19,9 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.log.LogcatEchoTracker
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogcatEchoTracker
 import com.google.common.truth.Truth.assertThat
 import java.io.PrintWriter
 import java.io.StringWriter
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
index d4b41c1..a363a03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
@@ -97,6 +97,7 @@
                         createUserRecord(2),
                         createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
                         createActionRecord(UserActionModel.ENTER_GUEST_MODE),
+                        createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT),
                     )
                 )
             var models: List<UserModel>? = null
@@ -176,15 +177,17 @@
                         createUserRecord(2),
                         createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
                         createActionRecord(UserActionModel.ENTER_GUEST_MODE),
+                        createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT),
                     )
                 )
             var models: List<UserActionModel>? = null
             val job = underTest.actions.onEach { models = it }.launchIn(this)
 
-            assertThat(models).hasSize(3)
+            assertThat(models).hasSize(4)
             assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER)
             assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER)
             assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE)
+            assertThat(models?.get(3)).isEqualTo(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
             job.cancel()
         }
 
@@ -200,6 +203,7 @@
             isAddUser = action == UserActionModel.ADD_USER,
             isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER,
             isGuest = action == UserActionModel.ENTER_GUEST_MODE,
+            isManageUsers = action == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
         )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
index c3a9705..6a17c8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
@@ -64,13 +64,7 @@
     @Test
     fun `actions - not actionable when locked and not locked`() =
         runBlocking(IMMEDIATE) {
-            userRepository.setActions(
-                listOf(
-                    UserActionModel.ENTER_GUEST_MODE,
-                    UserActionModel.ADD_USER,
-                    UserActionModel.ADD_SUPERVISED_USER,
-                )
-            )
+            setActions()
             userRepository.setActionableWhenLocked(false)
             keyguardRepository.setKeyguardShowing(false)
 
@@ -92,13 +86,7 @@
     @Test
     fun `actions - actionable when locked and not locked`() =
         runBlocking(IMMEDIATE) {
-            userRepository.setActions(
-                listOf(
-                    UserActionModel.ENTER_GUEST_MODE,
-                    UserActionModel.ADD_USER,
-                    UserActionModel.ADD_SUPERVISED_USER,
-                )
-            )
+            setActions()
             userRepository.setActionableWhenLocked(true)
             keyguardRepository.setKeyguardShowing(false)
 
@@ -120,13 +108,7 @@
     @Test
     fun `actions - actionable when locked and locked`() =
         runBlocking(IMMEDIATE) {
-            userRepository.setActions(
-                listOf(
-                    UserActionModel.ENTER_GUEST_MODE,
-                    UserActionModel.ADD_USER,
-                    UserActionModel.ADD_SUPERVISED_USER,
-                )
-            )
+            setActions()
             userRepository.setActionableWhenLocked(true)
             keyguardRepository.setKeyguardShowing(true)
 
@@ -182,6 +164,10 @@
         verify(activityStarter).startActivity(any(), anyBoolean())
     }
 
+    private fun setActions() {
+        userRepository.setActions(UserActionModel.values().toList())
+    }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index 0344e3f..c12a868 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -268,6 +268,26 @@
         }
 
     @Test
+    fun `menu actions`() =
+        runBlocking(IMMEDIATE) {
+            userRepository.setActions(UserActionModel.values().toList())
+            var actions: List<UserActionViewModel>? = null
+            val job = underTest.menu.onEach { actions = it }.launchIn(this)
+
+            assertThat(actions?.map { it.viewKey })
+                .isEqualTo(
+                    listOf(
+                        UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(),
+                        UserActionModel.ADD_USER.ordinal.toLong(),
+                        UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(),
+                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(),
+                    )
+                )
+
+            job.cancel()
+        }
+
+    @Test
     fun `isFinishRequested - finishes when user is switched`() =
         runBlocking(IMMEDIATE) {
             setUsers(count = 2)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt
deleted file mode 100644
index 5e09b81..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.util.collection
-
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertSame
-import org.junit.Assert.assertThrows
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class RingBufferTest : SysuiTestCase() {
-
-    private val buffer = RingBuffer(5) { TestElement() }
-
-    private val history = mutableListOf<TestElement>()
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-    }
-
-    @Test
-    fun testBarelyFillBuffer() {
-        fillBuffer(5)
-
-        assertEquals(0, buffer[0].id)
-        assertEquals(1, buffer[1].id)
-        assertEquals(2, buffer[2].id)
-        assertEquals(3, buffer[3].id)
-        assertEquals(4, buffer[4].id)
-    }
-
-    @Test
-    fun testPartiallyFillBuffer() {
-        fillBuffer(3)
-
-        assertEquals(3, buffer.size)
-
-        assertEquals(0, buffer[0].id)
-        assertEquals(1, buffer[1].id)
-        assertEquals(2, buffer[2].id)
-
-        assertThrows(IndexOutOfBoundsException::class.java) { buffer[3] }
-        assertThrows(IndexOutOfBoundsException::class.java) { buffer[4] }
-    }
-
-    @Test
-    fun testSpinBuffer() {
-        fillBuffer(277)
-
-        assertEquals(272, buffer[0].id)
-        assertEquals(273, buffer[1].id)
-        assertEquals(274, buffer[2].id)
-        assertEquals(275, buffer[3].id)
-        assertEquals(276, buffer[4].id)
-        assertThrows(IndexOutOfBoundsException::class.java) { buffer[5] }
-
-        assertEquals(5, buffer.size)
-    }
-
-    @Test
-    fun testElementsAreRecycled() {
-        fillBuffer(23)
-
-        assertSame(history[4], buffer[1])
-        assertSame(history[9], buffer[1])
-        assertSame(history[14], buffer[1])
-        assertSame(history[19], buffer[1])
-    }
-
-    @Test
-    fun testIterator() {
-        fillBuffer(13)
-
-        val iterator = buffer.iterator()
-
-        for (i in 0 until 5) {
-            assertEquals(history[8 + i], iterator.next())
-        }
-        assertFalse(iterator.hasNext())
-        assertThrows(NoSuchElementException::class.java) { iterator.next() }
-    }
-
-    @Test
-    fun testForEach() {
-        fillBuffer(13)
-        var i = 8
-
-        buffer.forEach {
-            assertEquals(history[i], it)
-            i++
-        }
-        assertEquals(13, i)
-    }
-
-    private fun fillBuffer(count: Int) {
-        for (i in 0 until count) {
-            val elem = buffer.advance()
-            elem.id = history.size
-            history.add(elem)
-        }
-    }
-}
-
-private class TestElement(var id: Int = 0) {
-    override fun toString(): String {
-        return "{TestElement $id}"
-    }
-}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index 202f4775..5d46de3 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -152,6 +152,8 @@
 
     // flag set by resource, whether to start dream immediately upon docking even if unlocked.
     private boolean mStartDreamImmediatelyOnDock = true;
+    // flag set by resource, whether to disable dreams when ambient mode suppression is enabled.
+    private boolean mDreamsDisabledByAmbientModeSuppression = false;
     // flag set by resource, whether to enable Car dock launch when starting car mode.
     private boolean mEnableCarDockLaunch = true;
     // flag set by resource, whether to lock UI mode to the default one or not.
@@ -364,6 +366,11 @@
         mStartDreamImmediatelyOnDock = startDreamImmediatelyOnDock;
     }
 
+    @VisibleForTesting
+    void setDreamsDisabledByAmbientModeSuppression(boolean disabledByAmbientModeSuppression) {
+        mDreamsDisabledByAmbientModeSuppression = disabledByAmbientModeSuppression;
+    }
+
     @Override
     public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
         mCurrentUser = to.getUserIdentifier();
@@ -424,6 +431,8 @@
         final Resources res = context.getResources();
         mStartDreamImmediatelyOnDock = res.getBoolean(
                 com.android.internal.R.bool.config_startDreamImmediatelyOnDock);
+        mDreamsDisabledByAmbientModeSuppression = res.getBoolean(
+                com.android.internal.R.bool.config_dreamsDisabledByAmbientModeSuppressionConfig);
         mNightMode = res.getInteger(
                 com.android.internal.R.integer.config_defaultNightMode);
         mDefaultUiModeType = res.getInteger(
@@ -1827,10 +1836,14 @@
         // Send the new configuration.
         applyConfigurationExternallyLocked();
 
+        final boolean dreamsSuppressed = mDreamsDisabledByAmbientModeSuppression
+                && mLocalPowerManager.isAmbientDisplaySuppressed();
+
         // If we did not start a dock app, then start dreaming if appropriate.
-        if (category != null && !dockAppStarted && (mStartDreamImmediatelyOnDock
-                || mWindowManager.isKeyguardShowingAndNotOccluded()
-                || !mPowerManager.isInteractive())) {
+        if (category != null && !dockAppStarted && !dreamsSuppressed && (
+                mStartDreamImmediatelyOnDock
+                        || mWindowManager.isKeyguardShowingAndNotOccluded()
+                        || !mPowerManager.isInteractive())) {
             mInjector.startDreamWhenDockedIfAppropriate(getContext());
         }
     }
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index 1e1ebeb..e421c61 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -44,6 +44,7 @@
 import android.annotation.UptimeMillisLong;
 import android.app.Activity;
 import android.app.ActivityManager;
+import android.app.BroadcastOptions;
 import android.app.IApplicationThread;
 import android.app.RemoteServiceException.CannotDeliverBroadcastException;
 import android.app.UidObserver;
@@ -534,6 +535,17 @@
             }, mBroadcastConsumerSkipAndCanceled, true);
         }
 
+        final int policy = (r.options != null)
+                ? r.options.getDeliveryGroupPolicy() : BroadcastOptions.DELIVERY_GROUP_POLICY_ALL;
+        if (policy == BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) {
+            forEachMatchingBroadcast(QUEUE_PREDICATE_ANY, (testRecord, testIndex) -> {
+                // We only allow caller to remove broadcasts they enqueued
+                return (r.callingUid == testRecord.callingUid)
+                        && (r.userId == testRecord.userId)
+                        && r.matchesDeliveryGroup(testRecord);
+            }, mBroadcastConsumerSkipAndCanceled, true);
+        }
+
         if (r.isReplacePending()) {
             // Leave the skipped broadcasts intact in queue, so that we can
             // replace them at their current position during enqueue below
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index 2d82595..4f64003 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -796,6 +796,16 @@
         }
     }
 
+    public boolean matchesDeliveryGroup(@NonNull BroadcastRecord other) {
+        final String key = (options != null) ? options.getDeliveryGroupKey() : null;
+        final String otherKey = (other.options != null)
+                ? other.options.getDeliveryGroupKey() : null;
+        if (key == null && otherKey == null) {
+            return intent.filterEquals(other.intent);
+        }
+        return Objects.equals(key, otherKey);
+    }
+
     @Override
     public String toString() {
         if (mCachedToString == null) {
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 216a48e..3fa41c0 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -16,7 +16,6 @@
 
 package com.android.server.am;
 
-import static android.Manifest.permission.CREATE_USERS;
 import static android.Manifest.permission.INTERACT_ACROSS_PROFILES;
 import static android.Manifest.permission.INTERACT_ACROSS_USERS;
 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
@@ -1482,7 +1481,7 @@
     // defined
     boolean startUserOnSecondaryDisplay(@UserIdInt int userId, int displayId) {
         checkCallingHasOneOfThosePermissions("startUserOnSecondaryDisplay",
-                MANAGE_USERS, CREATE_USERS);
+                MANAGE_USERS, INTERACT_ACROSS_USERS);
 
         // DEFAULT_DISPLAY is used for the current foreground user only
         Preconditions.checkArgument(displayId != Display.DEFAULT_DISPLAY,
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
index dec1b55..5bc9d23 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
@@ -54,7 +54,7 @@
     private AuthResultCoordinator mAuthResultCoordinator;
 
     public AuthSessionCoordinator() {
-        this(SystemClock.currentNetworkTimeClock());
+        this(SystemClock.elapsedRealtimeClock());
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java b/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java
index d9bd04d..6605d49 100644
--- a/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java
+++ b/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java
@@ -22,7 +22,6 @@
 import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK;
 
 import android.hardware.biometrics.BiometricManager;
-import android.os.SystemClock;
 import android.util.Slog;
 
 import java.time.Clock;
@@ -43,10 +42,6 @@
     private final Map<Integer, Map<Integer, AuthenticatorState>> mCanUserAuthenticate;
     private final Clock mClock;
 
-    MultiBiometricLockoutState() {
-        this(SystemClock.currentNetworkTimeClock());
-    }
-
     MultiBiometricLockoutState(Clock clock) {
         mCanUserAuthenticate = new HashMap<>();
         mClock = clock;
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index f599acac..2e5663d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -16,6 +16,7 @@
 
 package com.android.server.biometrics.sensors.fingerprint.aidl;
 
+import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR;
 
 import android.annotation.NonNull;
@@ -59,6 +60,7 @@
 import com.android.server.biometrics.sensors.fingerprint.PowerPressHandler;
 import com.android.server.biometrics.sensors.fingerprint.Udfps;
 
+import java.time.Clock;
 import java.util.ArrayList;
 import java.util.function.Supplier;
 
@@ -92,7 +94,9 @@
     private long mWaitForAuthKeyguard;
     private long mWaitForAuthBp;
     private long mIgnoreAuthFor;
+    private long mSideFpsLastAcquireStartTime;
     private Runnable mAuthSuccessRunnable;
+    private final Clock mClock;
 
     FingerprintAuthenticationClient(
             @NonNull Context context,
@@ -117,7 +121,8 @@
             boolean allowBackgroundAuthentication,
             @NonNull FingerprintSensorPropertiesInternal sensorProps,
             @NonNull Handler handler,
-            @Authenticators.Types int biometricStrength) {
+            @Authenticators.Types int biometricStrength,
+            @NonNull Clock clock) {
         super(
                 context,
                 lazyDaemon,
@@ -161,6 +166,8 @@
                         R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage);
         mBiometricStrength = biometricStrength;
         mAuthSessionCoordinator = biometricContext.getAuthSessionCoordinator();
+        mSideFpsLastAcquireStartTime = -1;
+        mClock = clock;
 
         if (mSensorProps.isAnySidefpsType()) {
             if (Build.isDebuggable()) {
@@ -246,8 +253,14 @@
                             return;
                         }
                         delay = isKeyguard() ? mWaitForAuthKeyguard : mWaitForAuthBp;
-                        Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps waiting for power for: "
-                                + delay + "ms");
+
+                        if (mSideFpsLastAcquireStartTime != -1) {
+                            delay = Math.max(0,
+                                    delay - (mClock.millis() - mSideFpsLastAcquireStartTime));
+                        }
+
+                        Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps "
+                                + "waiting for power until: " + delay + "ms");
                     }
 
                     if (mHandler.hasMessages(MESSAGE_FINGER_UP)) {
@@ -271,13 +284,15 @@
         mSensorOverlays.ifUdfps(controller -> controller.onAcquired(getSensorId(), acquiredInfo));
         super.onAcquired(acquiredInfo, vendorCode);
         if (mSensorProps.isAnySidefpsType()) {
+            if (acquiredInfo == FINGERPRINT_ACQUIRED_START) {
+                mSideFpsLastAcquireStartTime = mClock.millis();
+            }
             final boolean shouldLookForVendor =
                     mSkipWaitForPowerAcquireMessage == FINGERPRINT_ACQUIRED_VENDOR;
             final boolean acquireMessageMatch = acquiredInfo == mSkipWaitForPowerAcquireMessage;
             final boolean vendorMessageMatch = vendorCode == mSkipWaitForPowerVendorAcquireMessage;
             final boolean ignorePowerPress =
-                    (acquireMessageMatch && !shouldLookForVendor) || (shouldLookForVendor
-                            && acquireMessageMatch && vendorMessageMatch);
+                    acquireMessageMatch && (!shouldLookForVendor || vendorMessageMatch);
 
             if (ignorePowerPress) {
                 Slog.d(TAG, "(sideFPS) onFingerUp");
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
index 774aff1..650894d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
@@ -47,6 +47,7 @@
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.SystemClock;
 import android.os.UserManager;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -449,7 +450,8 @@
                     mTaskStackListener, mSensors.get(sensorId).getLockoutCache(),
                     mUdfpsOverlayController, mSidefpsController, allowBackgroundAuthentication,
                     mSensors.get(sensorId).getSensorProperties(), mHandler,
-                    Utils.getCurrentStrength(sensorId));
+                    Utils.getCurrentStrength(sensorId),
+                    SystemClock.elapsedRealtimeClock());
             scheduleForSensor(sensorId, client, mBiometricStateCallback);
         });
     }
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
index 0770062..6a01042 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
@@ -29,6 +29,8 @@
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl;
 import com.android.server.utils.Slogf;
 
 import java.io.FileDescriptor;
@@ -47,7 +49,7 @@
     private static final List<String> SERVICE_NAMES = Arrays.asList(
             IBroadcastRadio.DESCRIPTOR + "/amfm", IBroadcastRadio.DESCRIPTOR + "/dab");
 
-    private final com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl mHalAidl;
+    private final BroadcastRadioServiceImpl mHalAidl;
     private final BroadcastRadioService mService;
 
     /**
@@ -65,10 +67,15 @@
     }
 
     IRadioServiceAidlImpl(BroadcastRadioService service, ArrayList<String> serviceList) {
+        this(service, new BroadcastRadioServiceImpl(serviceList));
         Slogf.i(TAG, "Initialize BroadcastRadioServiceAidl(%s)", service);
-        mService = Objects.requireNonNull(service);
-        mHalAidl =
-                new com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl(serviceList);
+    }
+
+    @VisibleForTesting
+    IRadioServiceAidlImpl(BroadcastRadioService service, BroadcastRadioServiceImpl halAidl) {
+        mService = Objects.requireNonNull(service, "Broadcast radio service cannot be null");
+        mHalAidl = Objects.requireNonNull(halAidl,
+                "Broadcast radio service implementation for AIDL HAL cannot be null");
     }
 
     @Override
@@ -96,8 +103,8 @@
         if (isDebugEnabled()) {
             Slogf.d(TAG, "Adding announcement listener for %s", Arrays.toString(enabledTypes));
         }
-        Objects.requireNonNull(enabledTypes);
-        Objects.requireNonNull(listener);
+        Objects.requireNonNull(enabledTypes, "Enabled announcement types cannot be null");
+        Objects.requireNonNull(listener, "Announcement listener cannot be null");
         mService.enforcePolicyAccess();
 
         return mHalAidl.addAnnouncementListener(enabledTypes, listener);
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
index 28b6d02..a8e4034 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
@@ -27,6 +27,7 @@
 import android.util.Log;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.broadcastradio.hal2.AnnouncementAggregator;
 
 import java.io.FileDescriptor;
@@ -53,7 +54,7 @@
     private final List<RadioManager.ModuleProperties> mV1Modules;
 
     IRadioServiceHidlImpl(BroadcastRadioService service) {
-        mService = Objects.requireNonNull(service);
+        mService = Objects.requireNonNull(service, "broadcast radio service cannot be null");
         mHal1 = new com.android.server.broadcastradio.hal1.BroadcastRadioService(mLock);
         mV1Modules = mHal1.loadModules();
         OptionalInt max = mV1Modules.stream().mapToInt(RadioManager.ModuleProperties::getId).max();
@@ -61,6 +62,18 @@
                 max.isPresent() ? max.getAsInt() + 1 : 0, mLock);
     }
 
+    @VisibleForTesting
+    IRadioServiceHidlImpl(BroadcastRadioService service,
+            com.android.server.broadcastradio.hal1.BroadcastRadioService hal1,
+            com.android.server.broadcastradio.hal2.BroadcastRadioService hal2) {
+        mService = Objects.requireNonNull(service, "Broadcast radio service cannot be null");
+        mHal1 = Objects.requireNonNull(hal1,
+                "Broadcast radio service implementation for HIDL 1 HAL cannot be null");
+        mV1Modules = mHal1.loadModules();
+        mHal2 = Objects.requireNonNull(hal2,
+                "Broadcast radio service implementation for HIDL 2 HAL cannot be null");
+    }
+
     @Override
     public List<RadioManager.ModuleProperties> listModules() {
         mService.enforcePolicyAccess();
@@ -95,8 +108,8 @@
         if (isDebugEnabled()) {
             Slog.d(TAG, "Adding announcement listener for " + Arrays.toString(enabledTypes));
         }
-        Objects.requireNonNull(enabledTypes);
-        Objects.requireNonNull(listener);
+        Objects.requireNonNull(enabledTypes, "Enabled announcement types cannot be null");
+        Objects.requireNonNull(listener, "Announcement listener cannot be null");
         mService.enforcePolicyAccess();
 
         synchronized (mLock) {
diff --git a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
index e653f04..6f637b8 100644
--- a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
@@ -1447,7 +1447,9 @@
      * @return the cell ID or -1 if invalid
      */
     private static long getCidFromCellIdentity(CellIdentity id) {
-        if (id == null) return -1;
+        if (id == null) {
+            return -1;
+        }
         long cid = -1;
         switch(id.getType()) {
             case CellInfo.TYPE_GSM: cid = ((CellIdentityGsm) id).getCid(); break;
@@ -1522,7 +1524,8 @@
 
                 for (CellInfo ci : cil) {
                     int status = ci.getCellConnectionStatus();
-                    if (status == CellInfo.CONNECTION_PRIMARY_SERVING
+                    if (ci.isRegistered()
+                            || status == CellInfo.CONNECTION_PRIMARY_SERVING
                             || status == CellInfo.CONNECTION_SECONDARY_SERVING) {
                         CellIdentity c = ci.getCellIdentity();
                         int t = getCellType(ci);
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 5f06ca9..77dbde1 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -151,10 +151,7 @@
         mContext.registerReceiver(mScreenOnOffReceiver, screenOnOffIntentFilter);
     }
 
-    ////////////////////////////////////////////////////////////////
-    ////  Calls from MediaRouter2
-    ////   - Should not have @NonNull/@Nullable on any arguments
-    ////////////////////////////////////////////////////////////////
+    // Methods that implement MediaRouter2 operations.
 
     @NonNull
     public void enforceMediaContentControlPermission() {
@@ -242,7 +239,7 @@
         }
     }
 
-    public void registerRouter2(IMediaRouter2 router, String packageName) {
+    public void registerRouter2(@NonNull IMediaRouter2 router, @NonNull String packageName) {
         Objects.requireNonNull(router, "router must not be null");
         if (TextUtils.isEmpty(packageName)) {
             throw new IllegalArgumentException("packageName must not be empty");
@@ -269,7 +266,7 @@
         }
     }
 
-    public void unregisterRouter2(IMediaRouter2 router) {
+    public void unregisterRouter2(@NonNull IMediaRouter2 router) {
         Objects.requireNonNull(router, "router must not be null");
 
         final long token = Binder.clearCallingIdentity();
@@ -282,8 +279,8 @@
         }
     }
 
-    public void setDiscoveryRequestWithRouter2(IMediaRouter2 router,
-            RouteDiscoveryPreference preference) {
+    public void setDiscoveryRequestWithRouter2(@NonNull IMediaRouter2 router,
+            @NonNull RouteDiscoveryPreference preference) {
         Objects.requireNonNull(router, "router must not be null");
         Objects.requireNonNull(preference, "preference must not be null");
 
@@ -302,8 +299,8 @@
         }
     }
 
-    public void setRouteVolumeWithRouter2(IMediaRouter2 router,
-            MediaRoute2Info route, int volume) {
+    public void setRouteVolumeWithRouter2(@NonNull IMediaRouter2 router,
+            @NonNull MediaRoute2Info route, int volume) {
         Objects.requireNonNull(router, "router must not be null");
         Objects.requireNonNull(route, "route must not be null");
 
@@ -317,9 +314,9 @@
         }
     }
 
-    public void requestCreateSessionWithRouter2(IMediaRouter2 router, int requestId,
-            long managerRequestId, RoutingSessionInfo oldSession,
-            MediaRoute2Info route, Bundle sessionHints) {
+    public void requestCreateSessionWithRouter2(@NonNull IMediaRouter2 router, int requestId,
+            long managerRequestId, @NonNull RoutingSessionInfo oldSession,
+            @NonNull MediaRoute2Info route, Bundle sessionHints) {
         Objects.requireNonNull(router, "router must not be null");
         Objects.requireNonNull(oldSession, "oldSession must not be null");
         Objects.requireNonNull(route, "route must not be null");
@@ -335,8 +332,8 @@
         }
     }
 
-    public void selectRouteWithRouter2(IMediaRouter2 router, String uniqueSessionId,
-            MediaRoute2Info route) {
+    public void selectRouteWithRouter2(@NonNull IMediaRouter2 router,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
         Objects.requireNonNull(router, "router must not be null");
         Objects.requireNonNull(route, "route must not be null");
         if (TextUtils.isEmpty(uniqueSessionId)) {
@@ -353,8 +350,8 @@
         }
     }
 
-    public void deselectRouteWithRouter2(IMediaRouter2 router, String uniqueSessionId,
-            MediaRoute2Info route) {
+    public void deselectRouteWithRouter2(@NonNull IMediaRouter2 router,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
         Objects.requireNonNull(router, "router must not be null");
         Objects.requireNonNull(route, "route must not be null");
         if (TextUtils.isEmpty(uniqueSessionId)) {
@@ -371,8 +368,8 @@
         }
     }
 
-    public void transferToRouteWithRouter2(IMediaRouter2 router, String uniqueSessionId,
-            MediaRoute2Info route) {
+    public void transferToRouteWithRouter2(@NonNull IMediaRouter2 router,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
         Objects.requireNonNull(router, "router must not be null");
         Objects.requireNonNull(route, "route must not be null");
         if (TextUtils.isEmpty(uniqueSessionId)) {
@@ -389,8 +386,8 @@
         }
     }
 
-    public void setSessionVolumeWithRouter2(IMediaRouter2 router, String uniqueSessionId,
-            int volume) {
+    public void setSessionVolumeWithRouter2(@NonNull IMediaRouter2 router,
+            @NonNull String uniqueSessionId, int volume) {
         Objects.requireNonNull(router, "router must not be null");
         Objects.requireNonNull(uniqueSessionId, "uniqueSessionId must not be null");
 
@@ -404,7 +401,8 @@
         }
     }
 
-    public void releaseSessionWithRouter2(IMediaRouter2 router, String uniqueSessionId) {
+    public void releaseSessionWithRouter2(@NonNull IMediaRouter2 router,
+            @NonNull String uniqueSessionId) {
         Objects.requireNonNull(router, "router must not be null");
         if (TextUtils.isEmpty(uniqueSessionId)) {
             throw new IllegalArgumentException("uniqueSessionId must not be empty");
@@ -420,13 +418,10 @@
         }
     }
 
-    ////////////////////////////////////////////////////////////////
-    ////  Calls from MediaRouter2Manager
-    ////   - Should not have @NonNull/@Nullable on any arguments
-    ////////////////////////////////////////////////////////////////
+    // Methods that implement MediaRouter2Manager operations.
 
     @NonNull
-    public List<RoutingSessionInfo> getRemoteSessions(IMediaRouter2Manager manager) {
+    public List<RoutingSessionInfo> getRemoteSessions(@NonNull IMediaRouter2Manager manager) {
         Objects.requireNonNull(manager, "manager must not be null");
         final long token = Binder.clearCallingIdentity();
         try {
@@ -438,7 +433,8 @@
         }
     }
 
-    public void registerManager(IMediaRouter2Manager manager, String packageName) {
+    public void registerManager(@NonNull IMediaRouter2Manager manager,
+            @NonNull String packageName) {
         Objects.requireNonNull(manager, "manager must not be null");
         if (TextUtils.isEmpty(packageName)) {
             throw new IllegalArgumentException("packageName must not be empty");
@@ -458,7 +454,7 @@
         }
     }
 
-    public void unregisterManager(IMediaRouter2Manager manager) {
+    public void unregisterManager(@NonNull IMediaRouter2Manager manager) {
         Objects.requireNonNull(manager, "manager must not be null");
 
         final long token = Binder.clearCallingIdentity();
@@ -471,7 +467,7 @@
         }
     }
 
-    public void startScan(IMediaRouter2Manager manager) {
+    public void startScan(@NonNull IMediaRouter2Manager manager) {
         Objects.requireNonNull(manager, "manager must not be null");
         final long token = Binder.clearCallingIdentity();
         try {
@@ -483,7 +479,7 @@
         }
     }
 
-    public void stopScan(IMediaRouter2Manager manager) {
+    public void stopScan(@NonNull IMediaRouter2Manager manager) {
         Objects.requireNonNull(manager, "manager must not be null");
         final long token = Binder.clearCallingIdentity();
         try {
@@ -495,8 +491,8 @@
         }
     }
 
-    public void setRouteVolumeWithManager(IMediaRouter2Manager manager, int requestId,
-            MediaRoute2Info route, int volume) {
+    public void setRouteVolumeWithManager(@NonNull IMediaRouter2Manager manager, int requestId,
+            @NonNull MediaRoute2Info route, int volume) {
         Objects.requireNonNull(manager, "manager must not be null");
         Objects.requireNonNull(route, "route must not be null");
 
@@ -510,10 +506,11 @@
         }
     }
 
-    public void requestCreateSessionWithManager(IMediaRouter2Manager manager, int requestId,
-            RoutingSessionInfo oldSession, MediaRoute2Info route) {
+    public void requestCreateSessionWithManager(@NonNull IMediaRouter2Manager manager,
+            int requestId, @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route) {
         Objects.requireNonNull(manager, "manager must not be null");
         Objects.requireNonNull(oldSession, "oldSession must not be null");
+        Objects.requireNonNull(route, "route must not be null");
 
         final long token = Binder.clearCallingIdentity();
         try {
@@ -525,8 +522,8 @@
         }
     }
 
-    public void selectRouteWithManager(IMediaRouter2Manager manager, int requestId,
-            String uniqueSessionId, MediaRoute2Info route) {
+    public void selectRouteWithManager(@NonNull IMediaRouter2Manager manager, int requestId,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
         Objects.requireNonNull(manager, "manager must not be null");
         if (TextUtils.isEmpty(uniqueSessionId)) {
             throw new IllegalArgumentException("uniqueSessionId must not be empty");
@@ -543,8 +540,8 @@
         }
     }
 
-    public void deselectRouteWithManager(IMediaRouter2Manager manager, int requestId,
-            String uniqueSessionId, MediaRoute2Info route) {
+    public void deselectRouteWithManager(@NonNull IMediaRouter2Manager manager, int requestId,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
         Objects.requireNonNull(manager, "manager must not be null");
         if (TextUtils.isEmpty(uniqueSessionId)) {
             throw new IllegalArgumentException("uniqueSessionId must not be empty");
@@ -561,8 +558,8 @@
         }
     }
 
-    public void transferToRouteWithManager(IMediaRouter2Manager manager, int requestId,
-            String uniqueSessionId, MediaRoute2Info route) {
+    public void transferToRouteWithManager(@NonNull IMediaRouter2Manager manager, int requestId,
+            @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
         Objects.requireNonNull(manager, "manager must not be null");
         if (TextUtils.isEmpty(uniqueSessionId)) {
             throw new IllegalArgumentException("uniqueSessionId must not be empty");
@@ -579,8 +576,8 @@
         }
     }
 
-    public void setSessionVolumeWithManager(IMediaRouter2Manager manager, int requestId,
-            String uniqueSessionId, int volume) {
+    public void setSessionVolumeWithManager(@NonNull IMediaRouter2Manager manager, int requestId,
+            @NonNull String uniqueSessionId, int volume) {
         Objects.requireNonNull(manager, "manager must not be null");
         if (TextUtils.isEmpty(uniqueSessionId)) {
             throw new IllegalArgumentException("uniqueSessionId must not be empty");
@@ -596,8 +593,8 @@
         }
     }
 
-    public void releaseSessionWithManager(IMediaRouter2Manager manager, int requestId,
-            String uniqueSessionId) {
+    public void releaseSessionWithManager(@NonNull IMediaRouter2Manager manager, int requestId,
+            @NonNull String uniqueSessionId) {
         Objects.requireNonNull(manager, "manager must not be null");
         if (TextUtils.isEmpty(uniqueSessionId)) {
             throw new IllegalArgumentException("uniqueSessionId must not be empty");
@@ -681,11 +678,6 @@
         return mUserManagerInternal.getProfileParentId(userId) == mCurrentActiveUserId;
     }
 
-    ////////////////////////////////////////////////////////////////
-    ////  ***Locked methods related to MediaRouter2
-    ////   - Should have @NonNull/@Nullable on all arguments
-    ////////////////////////////////////////////////////////////////
-
     @GuardedBy("mLock")
     private void registerRouter2Locked(@NonNull IMediaRouter2 router, int uid, int pid,
             @NonNull String packageName, int userId, boolean hasConfigureWifiDisplayPermission,
@@ -960,11 +952,6 @@
                         DUMMY_REQUEST_ID, routerRecord, uniqueSessionId));
     }
 
-    ////////////////////////////////////////////////////////////
-    ////  ***Locked methods related to MediaRouter2Manager
-    ////   - Should have @NonNull/@Nullable on all arguments
-    ////////////////////////////////////////////////////////////
-
     private List<RoutingSessionInfo> getRemoteSessionsLocked(
             @NonNull IMediaRouter2Manager manager) {
         final IBinder binder = manager.asBinder();
@@ -1102,8 +1089,8 @@
     }
 
     private void requestCreateSessionWithManagerLocked(int requestId,
-            @NonNull IMediaRouter2Manager manager,
-            @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route) {
+            @NonNull IMediaRouter2Manager manager, @NonNull RoutingSessionInfo oldSession,
+            @NonNull MediaRoute2Info route) {
         ManagerRecord managerRecord = mAllManagerRecords.get(manager.asBinder());
         if (managerRecord == null) {
             return;
@@ -1250,8 +1237,7 @@
     }
 
     private void releaseSessionWithManagerLocked(int requestId,
-            @NonNull IMediaRouter2Manager manager,
-            @NonNull String uniqueSessionId) {
+            @NonNull IMediaRouter2Manager manager, @NonNull String uniqueSessionId) {
         final IBinder binder = manager.asBinder();
         ManagerRecord managerRecord = mAllManagerRecords.get(binder);
 
@@ -1274,11 +1260,6 @@
                         uniqueRequestId, routerRecord, uniqueSessionId));
     }
 
-    ////////////////////////////////////////////////////////////
-    ////  ***Locked methods used by both router2 and manager
-    ////   - Should have @NonNull/@Nullable on all arguments
-    ////////////////////////////////////////////////////////////
-
     @GuardedBy("mLock")
     private UserRecord getOrCreateUserRecordLocked(int userId) {
         UserRecord userRecord = mUserRecords.get(userId);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 77fea09..f459c0e 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -61,6 +61,7 @@
 import static android.content.pm.PackageManager.FEATURE_TELECOM;
 import static android.content.pm.PackageManager.FEATURE_TELEVISION;
 import static android.content.pm.PackageManager.MATCH_ALL;
+import static android.content.pm.PackageManager.MATCH_ANY_USER;
 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.PERMISSION_GRANTED;
@@ -10700,10 +10701,18 @@
         private final ArraySet<ManagedServiceInfo> mLightTrimListeners = new ArraySet<>();
         ArrayMap<Pair<ComponentName, Integer>, NotificationListenerFilter>
                 mRequestedNotificationListeners = new ArrayMap<>();
+        private final boolean mIsHeadlessSystemUserMode;
 
         public NotificationListeners(Context context, Object lock, UserProfiles userProfiles,
                 IPackageManager pm) {
+            this(context, lock, userProfiles, pm, UserManager.isHeadlessSystemUserMode());
+        }
+
+        @VisibleForTesting
+        public NotificationListeners(Context context, Object lock, UserProfiles userProfiles,
+                IPackageManager pm, boolean isHeadlessSystemUserMode) {
             super(context, lock, userProfiles, pm);
+            this.mIsHeadlessSystemUserMode = isHeadlessSystemUserMode;
         }
 
         @Override
@@ -10728,10 +10737,16 @@
                     if (TextUtils.isEmpty(listeners[i])) {
                         continue;
                     }
+                    int packageQueryFlags = MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE;
+                    // In the headless system user mode, packages might not be installed for the
+                    // system user. Match packages for any user since apps can be installed only for
+                    // non-system users and would be considering uninstalled for the system user.
+                    if (mIsHeadlessSystemUserMode) {
+                        packageQueryFlags += MATCH_ANY_USER;
+                    }
                     ArraySet<ComponentName> approvedListeners =
-                            this.queryPackageForServices(listeners[i],
-                                    MATCH_DIRECT_BOOT_AWARE
-                                            | MATCH_DIRECT_BOOT_UNAWARE, USER_SYSTEM);
+                            this.queryPackageForServices(listeners[i], packageQueryFlags,
+                                    USER_SYSTEM);
                     for (int k = 0; k < approvedListeners.size(); k++) {
                         ComponentName cn = approvedListeners.valueAt(k);
                         addDefaultComponentOrPackage(cn.flattenToString());
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index 3f04264..c4f6836 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -18,6 +18,7 @@
 
 import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
 
+import static com.android.server.LocalManagerRegistry.ManagerNotFoundException;
 import static com.android.server.pm.ApexManager.ActiveApexInfo;
 import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
 import static com.android.server.pm.PackageManagerService.DEBUG_DEXOPT;
@@ -34,6 +35,7 @@
 
 import android.Manifest;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.app.ActivityManager;
 import android.app.AppGlobals;
@@ -56,9 +58,16 @@
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.logging.MetricsLogger;
+import com.android.server.LocalManagerRegistry;
+import com.android.server.art.ArtManagerLocal;
+import com.android.server.art.model.ArtFlags;
+import com.android.server.art.model.OptimizeParams;
+import com.android.server.art.model.OptimizeResult;
+import com.android.server.pm.PackageDexOptimizer.DexOptResult;
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.DexoptOptions;
 import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.PackageState;
 import com.android.server.pm.pkg.PackageStateInternal;
 
 import dalvik.system.DexFile;
@@ -72,11 +81,15 @@
 import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
 
-final class DexOptHelper {
+/**
+ * Helper class for dex optimization operations in PackageManagerService.
+ */
+public final class DexOptHelper {
     private static final long SEVEN_DAYS_IN_MILLISECONDS = 7 * 24 * 60 * 60 * 1000;
 
     private final PackageManagerService mPm;
@@ -405,11 +418,12 @@
      * {@link PackageDexOptimizer#DEX_OPT_CANCELLED}
      * {@link PackageDexOptimizer#DEX_OPT_FAILED}
      */
-    @PackageDexOptimizer.DexOptResult
+    @DexOptResult
     /* package */ int performDexOptWithStatus(DexoptOptions options) {
         return performDexOptTraced(options);
     }
 
+    @DexOptResult
     private int performDexOptTraced(DexoptOptions options) {
         Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt");
         try {
@@ -421,7 +435,13 @@
 
     // Run dexopt on a given package. Returns true if dexopt did not fail, i.e.
     // if the package can now be considered up to date for the given filter.
+    @DexOptResult
     private int performDexOptInternal(DexoptOptions options) {
+        Optional<Integer> artSrvRes = performDexOptWithArtService(options);
+        if (artSrvRes.isPresent()) {
+            return artSrvRes.get();
+        }
+
         AndroidPackage p;
         PackageSetting pkgSetting;
         synchronized (mPm.mLock) {
@@ -446,8 +466,74 @@
         }
     }
 
-    private int performDexOptInternalWithDependenciesLI(AndroidPackage p,
-            @NonNull PackageStateInternal pkgSetting, DexoptOptions options) {
+    /**
+     * Performs dexopt on the given package using ART Service.
+     *
+     * @return a {@link DexOptResult}, or empty if the request isn't supported so that it is
+     *     necessary to fall back to the legacy code paths.
+     */
+    private Optional<Integer> performDexOptWithArtService(DexoptOptions options) {
+        ArtManagerLocal artManager = getArtManagerLocal();
+        if (artManager == null) {
+            return Optional.empty();
+        }
+
+        try (PackageManagerLocal.FilteredSnapshot snapshot =
+                        getPackageManagerLocal().withFilteredSnapshot()) {
+            PackageState ops = snapshot.getPackageState(options.getPackageName());
+            if (ops == null) {
+                return Optional.of(PackageDexOptimizer.DEX_OPT_FAILED);
+            }
+            AndroidPackage oap = ops.getAndroidPackage();
+            if (oap == null) {
+                return Optional.of(PackageDexOptimizer.DEX_OPT_FAILED);
+            }
+            if (oap.isApex()) {
+                return Optional.of(PackageDexOptimizer.DEX_OPT_SKIPPED);
+            }
+
+            // TODO(b/245301593): Delete the conditional when ART Service supports
+            // FLAG_SHOULD_INCLUDE_DEPENDENCIES and we can just set it unconditionally.
+            /*@OptimizeFlags*/ int extraFlags = ops.getUsesLibraries().isEmpty()
+                    ? 0
+                    : ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES;
+
+            OptimizeParams params = options.convertToOptimizeParams(extraFlags);
+            if (params == null) {
+                return Optional.empty();
+            }
+
+            // TODO(b/251903639): Either remove controlDexOptBlocking, or don't ignore it here.
+            OptimizeResult result;
+            try {
+                result = artManager.optimizePackage(snapshot, options.getPackageName(), params);
+            } catch (UnsupportedOperationException e) {
+                reportArtManagerFallback(options.getPackageName(), e.toString());
+                return Optional.empty();
+            }
+
+            // TODO(b/251903639): Move this to ArtManagerLocal.addOptimizePackageDoneCallback when
+            // it is implemented.
+            for (OptimizeResult.PackageOptimizeResult pkgRes : result.getPackageOptimizeResults()) {
+                PackageState ps = snapshot.getPackageState(pkgRes.getPackageName());
+                AndroidPackage ap = ps != null ? ps.getAndroidPackage() : null;
+                if (ap != null) {
+                    CompilerStats.PackageStats stats = mPm.getOrCreateCompilerPackageStats(ap);
+                    for (OptimizeResult.DexContainerFileOptimizeResult dexRes :
+                            pkgRes.getDexContainerFileOptimizeResults()) {
+                        stats.setCompileTime(
+                                dexRes.getDexContainerFile(), dexRes.getDex2oatWallTimeMillis());
+                    }
+                }
+            }
+
+            return Optional.of(convertToDexOptResult(result));
+        }
+    }
+
+    @DexOptResult
+    private int performDexOptInternalWithDependenciesLI(
+            AndroidPackage p, @NonNull PackageStateInternal pkgSetting, DexoptOptions options) {
         // System server gets a special path.
         if (PLATFORM_PACKAGE_NAME.equals(p.getPackageName())) {
             return mPm.getDexManager().dexoptSystemServer(options);
@@ -514,10 +600,20 @@
 
         // Whoever is calling forceDexOpt wants a compiled package.
         // Don't use profiles since that may cause compilation to be skipped.
-        final int res = performDexOptInternalWithDependenciesLI(pkg, packageState,
-                new DexoptOptions(packageName, REASON_CMDLINE,
-                        getDefaultCompilerFilter(), null /* splitName */,
-                        DexoptOptions.DEXOPT_FORCE | DexoptOptions.DEXOPT_BOOT_COMPLETE));
+        DexoptOptions options = new DexoptOptions(packageName, REASON_CMDLINE,
+                getDefaultCompilerFilter(), null /* splitName */,
+                DexoptOptions.DEXOPT_FORCE | DexoptOptions.DEXOPT_BOOT_COMPLETE);
+
+        // performDexOptWithArtService ignores the snapshot and takes its own, so it can race with
+        // the package checks above, but at worst the effect is only a bit less friendly error
+        // below.
+        Optional<Integer> artSrvRes = performDexOptWithArtService(options);
+        int res;
+        if (artSrvRes.isPresent()) {
+            res = artSrvRes.get();
+        } else {
+            res = performDexOptInternalWithDependenciesLI(pkg, packageState, options);
+        }
 
         Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
         if (res != PackageDexOptimizer.DEX_OPT_PERFORMED) {
@@ -800,4 +896,59 @@
         }
         return false;
     }
+
+    private @NonNull PackageManagerLocal getPackageManagerLocal() {
+        try {
+            return LocalManagerRegistry.getManagerOrThrow(PackageManagerLocal.class);
+        } catch (ManagerNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Called whenever we need to fall back from ART Service to the legacy dexopt code.
+     */
+    public static void reportArtManagerFallback(String packageName, String reason) {
+        // STOPSHIP(b/251903639): Minimize these calls to avoid platform getting shipped with code
+        // paths that will always bypass ART Service.
+        Slog.i(TAG, "Falling back to old PackageManager dexopt for " + packageName + ": " + reason);
+    }
+
+    /**
+     * Returns {@link ArtManagerLocal} if one is found and should be used for package optimization.
+     */
+    private @Nullable ArtManagerLocal getArtManagerLocal() {
+        if (!"true".equals(SystemProperties.get("dalvik.vm.useartservice", ""))) {
+            return null;
+        }
+        try {
+            return LocalManagerRegistry.getManagerOrThrow(ArtManagerLocal.class);
+        } catch (ManagerNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Converts an ART Service {@link OptimizeResult} to {@link DexOptResult}.
+     *
+     * For interfacing {@link ArtManagerLocal} with legacy dex optimization code in PackageManager.
+     */
+    @DexOptResult
+    private static int convertToDexOptResult(OptimizeResult result) {
+        /*@OptimizeStatus*/ int status = result.getFinalStatus();
+        switch (status) {
+            case OptimizeResult.OPTIMIZE_SKIPPED:
+                return PackageDexOptimizer.DEX_OPT_SKIPPED;
+            case OptimizeResult.OPTIMIZE_FAILED:
+                return PackageDexOptimizer.DEX_OPT_FAILED;
+            case OptimizeResult.OPTIMIZE_PERFORMED:
+                return PackageDexOptimizer.DEX_OPT_PERFORMED;
+            case OptimizeResult.OPTIMIZE_CANCELLED:
+                return PackageDexOptimizer.DEX_OPT_CANCELLED;
+            default:
+                throw new IllegalArgumentException("OptimizeResult for "
+                        + result.getPackageOptimizeResults().get(0).getPackageName()
+                        + " has unsupported status " + status);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
index d25bca7..2a2410fd 100644
--- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java
+++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
@@ -652,12 +652,6 @@
     @DexOptResult
     private int dexOptSecondaryDexPathLI(ApplicationInfo info, String path,
             PackageDexUsage.DexUseInfo dexUseInfo, DexoptOptions options) {
-        if (options.isDexoptOnlySharedDex() && !dexUseInfo.isUsedByOtherApps()) {
-            // We are asked to optimize only the dex files used by other apps and this is not
-            // on of them: skip it.
-            return DEX_OPT_SKIPPED;
-        }
-
         String compilerFilter = getRealCompilerFilter(info, options.getCompilerFilter(),
                 dexUseInfo.isUsedByOtherApps());
         // Get the dexopt flags after getRealCompilerFilter to make sure we get the correct flags.
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 60f2478..2119191 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -2633,6 +2633,9 @@
     /** @return a specific user restriction that's in effect currently. */
     @Override
     public boolean hasUserRestriction(String restrictionKey, @UserIdInt int userId) {
+        if (!userExists(userId)) {
+            return false;
+        }
         checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, "hasUserRestriction");
         return mLocalService.hasUserRestriction(restrictionKey, userId);
     }
@@ -5516,6 +5519,13 @@
 
     private void removeUserState(final @UserIdInt int userId) {
         Slog.i(LOG_TAG, "Removing user state of user " + userId);
+
+        // Cleanup lock settings.  This must happen before destroyUserKey(), since the user's DE
+        // storage must still be accessible for the lock settings state to be properly cleaned up.
+        mLockPatternUtils.removeUser(userId);
+
+        // Evict and destroy the user's CE and DE encryption keys.  At this point, the user's CE and
+        // DE storage is made inaccessible, except to delete its contents.
         try {
             mContext.getSystemService(StorageManager.class).destroyUserKey(userId);
         } catch (IllegalStateException e) {
@@ -5523,9 +5533,6 @@
             Slog.i(LOG_TAG, "Destroying key for user " + userId + " failed, continuing anyway", e);
         }
 
-        // Cleanup lock settings
-        mLockPatternUtils.removeUser(userId);
-
         // Cleanup package manager settings
         mPm.cleanUpUser(this, userId);
 
diff --git a/services/core/java/com/android/server/pm/dex/DexoptOptions.java b/services/core/java/com/android/server/pm/dex/DexoptOptions.java
index ea23316..f5557c4 100644
--- a/services/core/java/com/android/server/pm/dex/DexoptOptions.java
+++ b/services/core/java/com/android/server/pm/dex/DexoptOptions.java
@@ -18,6 +18,16 @@
 
 import static com.android.server.pm.PackageManagerServiceCompilerMapping.getCompilerFilterForReason;
 
+import android.annotation.Nullable;
+
+import com.android.server.art.ReasonMapping;
+import com.android.server.art.model.ArtFlags;
+import com.android.server.art.model.OptimizeParams;
+import com.android.server.pm.DexOptHelper;
+import com.android.server.pm.PackageManagerService;
+
+import dalvik.system.DexFile;
+
 /**
  * Options used for dexopt invocations.
  */
@@ -40,10 +50,6 @@
     // will only consider the primary apk.
     public static final int DEXOPT_ONLY_SECONDARY_DEX = 1 << 3;
 
-    // When set, dexopt will optimize only dex files that are used by other apps.
-    // Currently, this flag is ignored for primary apks.
-    public static final int DEXOPT_ONLY_SHARED_DEX = 1 << 4;
-
     // When set, dexopt will attempt to scale down the optimizations previously applied in order
     // save disk space.
     public static final int DEXOPT_DOWNGRADE = 1 << 5;
@@ -105,7 +111,6 @@
                 DEXOPT_FORCE |
                 DEXOPT_BOOT_COMPLETE |
                 DEXOPT_ONLY_SECONDARY_DEX |
-                DEXOPT_ONLY_SHARED_DEX |
                 DEXOPT_DOWNGRADE |
                 DEXOPT_AS_SHARED_LIBRARY |
                 DEXOPT_IDLE_BACKGROUND_JOB |
@@ -146,10 +151,6 @@
         return (mFlags & DEXOPT_ONLY_SECONDARY_DEX) != 0;
     }
 
-    public boolean isDexoptOnlySharedDex() {
-        return (mFlags & DEXOPT_ONLY_SHARED_DEX) != 0;
-    }
-
     public boolean isDowngrade() {
         return (mFlags & DEXOPT_DOWNGRADE) != 0;
     }
@@ -198,4 +199,133 @@
                 mSplitName,
                 mFlags);
     }
+
+    /**
+     * Returns an {@link OptimizeParams} instance corresponding to this object, for use with
+     * {@link com.android.server.art.ArtManagerLocal}.
+     *
+     * @param extraFlags extra {@link ArtFlags#OptimizeFlags} to set in the returned
+     *     {@code OptimizeParams} beyond those converted from this object
+     * @return null if the settings cannot be accurately represented, and hence the old
+     *     PackageManager/installd code paths need to be used.
+     */
+    public @Nullable OptimizeParams convertToOptimizeParams(/*@OptimizeFlags*/ int extraFlags) {
+        if (mSplitName != null) {
+            DexOptHelper.reportArtManagerFallback(
+                    mPackageName, "Request to optimize only split " + mSplitName);
+            return null;
+        }
+
+        /*@OptimizeFlags*/ int flags = extraFlags;
+        if ((mFlags & DEXOPT_CHECK_FOR_PROFILES_UPDATES) == 0
+                && DexFile.isProfileGuidedCompilerFilter(mCompilerFilter)) {
+            // ART Service doesn't support bypassing this, so not setting this flag is not
+            // supported.
+            DexOptHelper.reportArtManagerFallback(mPackageName,
+                    "DEXOPT_CHECK_FOR_PROFILES_UPDATES not set with profile compiler filter");
+            return null;
+        }
+        if ((mFlags & DEXOPT_FORCE) != 0) {
+            flags |= ArtFlags.FLAG_FORCE;
+        }
+        if ((mFlags & DEXOPT_ONLY_SECONDARY_DEX) != 0) {
+            flags |= ArtFlags.FLAG_FOR_SECONDARY_DEX;
+        } else {
+            flags |= ArtFlags.FLAG_FOR_PRIMARY_DEX;
+        }
+        if ((mFlags & DEXOPT_DOWNGRADE) != 0) {
+            flags |= ArtFlags.FLAG_SHOULD_DOWNGRADE;
+        }
+        if ((mFlags & DEXOPT_INSTALL_WITH_DEX_METADATA_FILE) == 0) {
+            // ART Service cannot be instructed to ignore a DM file if present, so not setting this
+            // flag is not supported.
+            DexOptHelper.reportArtManagerFallback(
+                    mPackageName, "DEXOPT_INSTALL_WITH_DEX_METADATA_FILE not set");
+            return null;
+        }
+
+        /*@PriorityClassApi*/ int priority;
+        // Replicates logic in RunDex2Oat::PrepareCompilerRuntimeAndPerfConfigFlags in installd.
+        if ((mFlags & DEXOPT_BOOT_COMPLETE) != 0) {
+            if ((mFlags & DEXOPT_FOR_RESTORE) != 0) {
+                priority = ArtFlags.PRIORITY_INTERACTIVE_FAST;
+            } else {
+                // TODO(b/251903639): Repurpose DEXOPT_IDLE_BACKGROUND_JOB to choose new
+                // dalvik.vm.background-dex2oat-* properties.
+                priority = ArtFlags.PRIORITY_INTERACTIVE;
+            }
+        } else {
+            priority = ArtFlags.PRIORITY_BOOT;
+        }
+
+        // The following flags in mFlags are ignored:
+        //
+        // -  DEXOPT_AS_SHARED_LIBRARY: It's implicit with ART Service since it always looks at
+        //    <uses-library> rather than actual dependencies.
+        //
+        //    We don't require it to be set either. It's safe when switching between old and new
+        //    code paths since the only effect is that some packages may be unnecessarily compiled
+        //    without user profiles.
+        //
+        // -  DEXOPT_IDLE_BACKGROUND_JOB: Its only effect is to allow the debug variant dex2oatd to
+        //    be used, but ART Service never uses that (cf. Artd::GetDex2Oat in artd.cc).
+
+        String reason;
+        switch (mCompilationReason) {
+            case PackageManagerService.REASON_FIRST_BOOT:
+                reason = ReasonMapping.REASON_FIRST_BOOT;
+                break;
+            case PackageManagerService.REASON_BOOT_AFTER_OTA:
+                reason = ReasonMapping.REASON_BOOT_AFTER_OTA;
+                break;
+            case PackageManagerService.REASON_POST_BOOT:
+                // This reason will go away with the legacy dexopt code.
+                DexOptHelper.reportArtManagerFallback(
+                        mPackageName, "Unsupported compilation reason REASON_POST_BOOT");
+                return null;
+            case PackageManagerService.REASON_INSTALL:
+                reason = ReasonMapping.REASON_INSTALL;
+                break;
+            case PackageManagerService.REASON_INSTALL_FAST:
+                reason = ReasonMapping.REASON_INSTALL_FAST;
+                break;
+            case PackageManagerService.REASON_INSTALL_BULK:
+                reason = ReasonMapping.REASON_INSTALL_BULK;
+                break;
+            case PackageManagerService.REASON_INSTALL_BULK_SECONDARY:
+                reason = ReasonMapping.REASON_INSTALL_BULK_SECONDARY;
+                break;
+            case PackageManagerService.REASON_INSTALL_BULK_DOWNGRADED:
+                reason = ReasonMapping.REASON_INSTALL_BULK_DOWNGRADED;
+                break;
+            case PackageManagerService.REASON_INSTALL_BULK_SECONDARY_DOWNGRADED:
+                reason = ReasonMapping.REASON_INSTALL_BULK_SECONDARY_DOWNGRADED;
+                break;
+            case PackageManagerService.REASON_BACKGROUND_DEXOPT:
+                reason = ReasonMapping.REASON_BG_DEXOPT;
+                break;
+            case PackageManagerService.REASON_INACTIVE_PACKAGE_DOWNGRADE:
+                reason = ReasonMapping.REASON_INACTIVE;
+                break;
+            case PackageManagerService.REASON_CMDLINE:
+                reason = ReasonMapping.REASON_CMDLINE;
+                break;
+            case PackageManagerService.REASON_SHARED:
+            case PackageManagerService.REASON_AB_OTA:
+                // REASON_SHARED shouldn't go into this code path - it's only used at lower levels
+                // in PackageDexOptimizer.
+                // TODO(b/251921228): OTA isn't supported, so REASON_AB_OTA shouldn't come this way
+                // either.
+                throw new UnsupportedOperationException(
+                        "ART Service unsupported compilation reason " + mCompilationReason);
+            default:
+                throw new IllegalArgumentException(
+                        "Invalid compilation reason " + mCompilationReason);
+        }
+
+        return new OptimizeParams.Builder(reason, flags)
+                .setCompilerFilter(mCompilerFilter)
+                .setPriorityClass(priority)
+                .build();
+    }
 }
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 5abc875..4784723 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -6768,6 +6768,11 @@
         public void nap(long eventTime, boolean allowWake) {
             napInternal(eventTime, Process.SYSTEM_UID, allowWake);
         }
+
+        @Override
+        public boolean isAmbientDisplaySuppressed() {
+            return mAmbientDisplaySuppressionController.isSuppressed();
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 214a2c1..3c457e1 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -2578,6 +2578,9 @@
                         // activity lifecycle transaction to make sure the override pending app
                         // transition will be applied immediately.
                         targetActivity.applyOptionsAnimation();
+                        if (activityOptions != null && activityOptions.getLaunchCookie() != null) {
+                            targetActivity.mLaunchCookie = activityOptions.getLaunchCookie();
+                        }
                     } finally {
                         mActivityMetricsLogger.notifyActivityLaunched(launchingState,
                                 START_TASK_TO_FRONT, false /* newActivityCreated */,
diff --git a/services/core/jni/gnss/AGnssRil.cpp b/services/core/jni/gnss/AGnssRil.cpp
index 424ffd4..34e4976 100644
--- a/services/core/jni/gnss/AGnssRil.cpp
+++ b/services/core/jni/gnss/AGnssRil.cpp
@@ -55,13 +55,13 @@
         case IAGnssRil::AGnssRefLocationType::UMTS_CELLID:
         case IAGnssRil::AGnssRefLocationType::LTE_CELLID:
         case IAGnssRil::AGnssRefLocationType::NR_CELLID:
-            location.cellID.mcc = mcc;
-            location.cellID.mnc = mnc;
-            location.cellID.lac = lac;
-            location.cellID.cid = cid;
-            location.cellID.tac = tac;
-            location.cellID.pcid = pcid;
-            location.cellID.arfcn = arfcn;
+            location.cellID.mcc = static_cast<int>(mcc);
+            location.cellID.mnc = static_cast<int>(mnc);
+            location.cellID.lac = static_cast<int>(lac);
+            location.cellID.cid = static_cast<long>(cid);
+            location.cellID.tac = static_cast<int>(tac);
+            location.cellID.pcid = static_cast<int>(pcid);
+            location.cellID.arfcn = static_cast<int>(arfcn);
             break;
         default:
             ALOGE("Unknown cellid (%s:%d).", __FUNCTION__, __LINE__);
@@ -106,20 +106,24 @@
     return checkHidlReturn(result, "IAGnssRil_V1_0 setSetId() failed.");
 }
 
-jboolean AGnssRil_V1_0::setRefLocation(jint type, jint mcc, jint mnc, jint lac, jlong cid, jint,
-                                       jint, jint) {
+jboolean AGnssRil_V1_0::setRefLocation(jint type, jint mcc, jint mnc, jint lac, jlong cid, jint tac,
+                                       jint pcid, jint) {
     IAGnssRil_V1_0::AGnssRefLocation location;
-    switch (static_cast<IAGnssRil_V1_0::AGnssRefLocationType>(type)) {
+    location.type = static_cast<IAGnssRil_V1_0::AGnssRefLocationType>(type);
+
+    switch (location.type) {
         case IAGnssRil_V1_0::AGnssRefLocationType::GSM_CELLID:
         case IAGnssRil_V1_0::AGnssRefLocationType::UMTS_CELLID:
-            location.type = static_cast<IAGnssRil_V1_0::AGnssRefLocationType>(type);
-            location.cellID.mcc = mcc;
-            location.cellID.mnc = mnc;
-            location.cellID.lac = lac;
-            location.cellID.cid = cid;
+        case IAGnssRil_V1_0::AGnssRefLocationType::LTE_CELLID:
+            location.cellID.mcc = static_cast<uint16_t>(mcc);
+            location.cellID.mnc = static_cast<uint16_t>(mnc);
+            location.cellID.lac = static_cast<uint16_t>(lac);
+            location.cellID.cid = static_cast<uint32_t>(cid);
+            location.cellID.tac = static_cast<uint16_t>(tac);
+            location.cellID.pcid = static_cast<uint16_t>(pcid);
             break;
         default:
-            ALOGE("Neither a GSM nor a UMTS cellid (%s:%d).", __FUNCTION__, __LINE__);
+            ALOGE("Unknown cellid (%s:%d).", __FUNCTION__, __LINE__);
             return JNI_FALSE;
             break;
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index 90b1f4e..abc32c9 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -39,6 +39,8 @@
 import android.app.BroadcastOptions;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.os.Bundle;
 import android.os.HandlerThread;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -386,4 +388,86 @@
         assertEquals(Intent.ACTION_SCREEN_OFF, queue.getActive().intent.getAction());
         assertTrue(queue.isEmpty());
     }
+
+    /**
+     * Verify that sending a broadcast with DELIVERY_GROUP_POLICY_MOST_RECENT works as expected.
+     */
+    @Test
+    public void testDeliveryGroupPolicy_mostRecent() {
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+        final BroadcastOptions optionsTimeTick = BroadcastOptions.makeBasic();
+        optionsTimeTick.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT);
+
+        final Intent musicVolumeChanged = new Intent(AudioManager.VOLUME_CHANGED_ACTION);
+        musicVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
+                AudioManager.STREAM_MUSIC);
+        final BroadcastOptions optionsMusicVolumeChanged = BroadcastOptions.makeBasic();
+        optionsMusicVolumeChanged.setDeliveryGroupPolicy(
+                BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT);
+        optionsMusicVolumeChanged.setDeliveryGroupKey("audio",
+                String.valueOf(AudioManager.STREAM_MUSIC));
+
+        final Intent alarmVolumeChanged = new Intent(AudioManager.VOLUME_CHANGED_ACTION);
+        alarmVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
+                AudioManager.STREAM_ALARM);
+        final BroadcastOptions optionsAlarmVolumeChanged = BroadcastOptions.makeBasic();
+        optionsAlarmVolumeChanged.setDeliveryGroupPolicy(
+                BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT);
+        optionsAlarmVolumeChanged.setDeliveryGroupKey("audio",
+                String.valueOf(AudioManager.STREAM_ALARM));
+
+        // Halt all processing so that we get a consistent view
+        mHandlerThread.getLooper().getQueue().postSyncBarrier();
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged,
+                optionsMusicVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged,
+                optionsAlarmVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged,
+                optionsMusicVolumeChanged));
+
+        final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        // Verify that the older musicVolumeChanged has been removed.
+        verifyPendingRecords(queue,
+                List.of(timeTick, alarmVolumeChanged, musicVolumeChanged));
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged,
+                optionsAlarmVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged,
+                optionsMusicVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged,
+                optionsAlarmVolumeChanged));
+        // Verify that the older alarmVolumeChanged has been removed.
+        verifyPendingRecords(queue,
+                List.of(timeTick, musicVolumeChanged, alarmVolumeChanged));
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(musicVolumeChanged,
+                optionsMusicVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(alarmVolumeChanged,
+                optionsAlarmVolumeChanged));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(timeTick, optionsTimeTick));
+        // Verify that the older timeTick has been removed.
+        verifyPendingRecords(queue,
+                List.of(musicVolumeChanged, alarmVolumeChanged, timeTick));
+    }
+
+    private void verifyPendingRecords(BroadcastProcessQueue queue,
+            List<Intent> intents) {
+        for (int i = 0; i < intents.size(); i++) {
+            queue.makeActiveNextPending();
+            final Intent actualIntent = queue.getActive().intent;
+            final Intent expectedIntent = intents.get(i);
+            final String errMsg = "actual=" + actualIntent + ", expected=" + expectedIntent
+                    + ", actual_extras=" + actualIntent.getExtras()
+                    + ", expected_extras=" + expectedIntent.getExtras();
+            assertTrue(errMsg, actualIntent.filterEquals(expectedIntent));
+            assertTrue(errMsg, Bundle.kindofEquals(
+                    actualIntent.getExtras(), expectedIntent.getExtras()));
+        }
+        assertTrue(queue.isEmpty());
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index 73548a3..1b5db0a 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -41,6 +41,7 @@
 import android.hardware.biometrics.fingerprint.ISession;
 import android.hardware.biometrics.fingerprint.PointerContext;
 import android.hardware.fingerprint.Fingerprint;
+import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.ISidefpsController;
 import android.hardware.fingerprint.IUdfpsOverlayController;
@@ -74,6 +75,7 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
+import java.time.Clock;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Consumer;
@@ -131,6 +133,8 @@
     private Probe mLuxProbe;
     @Mock
     private AuthSessionCoordinator mAuthSessionCoordinator;
+    @Mock
+    private Clock mClock;
     @Captor
     private ArgumentCaptor<OperationContext> mOperationContextCaptor;
     @Captor
@@ -451,6 +455,52 @@
     }
 
     @Test
+    public void sideFingerprintSkipsWindowIfVendorMessageMatch() throws Exception {
+        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+        final int vendorAcquireMessage = 1234;
+
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsSkipWaitForPowerAcquireMessage,
+                FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage,
+                vendorAcquireMessage);
+
+        final FingerprintAuthenticationClient client = createClient(1);
+        client.start(mCallback);
+        mLooper.dispatchAll();
+        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+                true /* authenticated */, new ArrayList<>());
+        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, vendorAcquireMessage);
+        mLooper.dispatchAll();
+
+        verify(mCallback).onClientFinished(any(), eq(true));
+    }
+
+    @Test
+    public void sideFingerprintDoesNotSkipWindowOnVendorErrorMismatch() throws Exception {
+        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+        final int vendorAcquireMessage = 1234;
+
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsSkipWaitForPowerAcquireMessage,
+                FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage,
+                vendorAcquireMessage);
+
+        final FingerprintAuthenticationClient client = createClient(1);
+        client.start(mCallback);
+        mLooper.dispatchAll();
+        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+                true /* authenticated */, new ArrayList<>());
+        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, 1);
+        mLooper.dispatchAll();
+
+        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
+    }
+
+    @Test
     public void sideFingerprintSendsAuthIfFingerUp() throws Exception {
         when(mSensorProps.isAnySidefpsType()).thenReturn(true);
 
@@ -497,6 +547,79 @@
         verify(mCallback).onClientFinished(any(), eq(true));
     }
 
+    @Test
+    public void sideFingerprintPowerWindowStartsOnAcquireStart() throws Exception {
+        final int powerWindow = 500;
+        final long authStart = 300;
+
+        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsBpPowerPressWindow, powerWindow);
+
+        final FingerprintAuthenticationClient client = createClient(1);
+        client.start(mCallback);
+
+        // Acquire start occurs at time = 0ms
+        when(mClock.millis()).thenReturn(0L);
+        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
+
+        // Auth occurs at time = 300
+        when(mClock.millis()).thenReturn(authStart);
+        // At this point the delay should be 500 - (300 - 0) == 200 milliseconds.
+        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+                true /* authenticated */, new ArrayList<>());
+        mLooper.dispatchAll();
+        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
+
+        // After waiting 200 milliseconds, auth should succeed.
+        mLooper.moveTimeForward(powerWindow - authStart);
+        mLooper.dispatchAll();
+        verify(mCallback).onClientFinished(any(), eq(true));
+    }
+
+    @Test
+    public void sideFingerprintPowerWindowStartsOnLastAcquireStart() throws Exception {
+        final int powerWindow = 500;
+
+        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.integer.config_sidefpsBpPowerPressWindow, powerWindow);
+
+        final FingerprintAuthenticationClient client = createClient(1);
+        client.start(mCallback);
+        // Acquire start occurs at time = 0ms
+        when(mClock.millis()).thenReturn(0L);
+        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
+
+        // Auth reject occurs at time = 300ms
+        when(mClock.millis()).thenReturn(300L);
+        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+                false /* authenticated */, new ArrayList<>());
+        mLooper.dispatchAll();
+
+        mLooper.moveTimeForward(300);
+        mLooper.dispatchAll();
+        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
+
+        when(mClock.millis()).thenReturn(1300L);
+        client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */);
+
+        // If code is correct, the new acquired start timestamp should be used
+        // and the code should only have to wait 500 - (1500-1300)ms.
+        when(mClock.millis()).thenReturn(1500L);
+        client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */),
+                true /* authenticated */, new ArrayList<>());
+        mLooper.dispatchAll();
+
+        mLooper.moveTimeForward(299);
+        mLooper.dispatchAll();
+        verify(mCallback, never()).onClientFinished(any(), anyBoolean());
+
+        mLooper.moveTimeForward(1);
+        mLooper.dispatchAll();
+        verify(mCallback).onClientFinished(any(), eq(true));
+    }
+
     private FingerprintAuthenticationClient createClient() throws RemoteException {
         return createClient(100 /* version */, true /* allowBackgroundAuthentication */);
     }
@@ -524,7 +647,7 @@
                 null /* taskStackListener */, mLockoutCache,
                 mUdfpsOverlayController, mSideFpsController, allowBackgroundAuthentication,
                 mSensorProps,
-                new Handler(mLooper.getLooper()), 0 /* biometricStrength */) {
+                new Handler(mLooper.getLooper()), 0 /* biometricStrength */, mClock) {
             @Override
             protected ActivityTaskManager getActivityTaskManager() {
                 return mActivityTaskManager;
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 96707fd..00aa520 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -176,6 +176,13 @@
     }
 
     @Test
+    public void testHasUserRestriction_NonExistentUserReturnsFalse() {
+        int nonExistentUserId = UserHandle.USER_NULL;
+        assertThat(mUserManagerService.hasUserRestriction(DISALLOW_USER_SWITCH, nonExistentUserId))
+                .isFalse();
+    }
+
+    @Test
     public void testSetUserRestrictionWithIncorrectID() throws Exception {
         int incorrectId = 1;
         while (mUserManagerService.userExists(incorrectId)) {
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexoptOptionsTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexoptOptionsTests.java
index d5893c8..77d542a 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/DexoptOptionsTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexoptOptionsTests.java
@@ -52,7 +52,6 @@
         assertFalse(opt.isBootComplete());
         assertFalse(opt.isCheckForProfileUpdates());
         assertFalse(opt.isDexoptOnlySecondaryDex());
-        assertFalse(opt.isDexoptOnlySharedDex());
         assertFalse(opt.isDowngrade());
         assertFalse(opt.isForce());
         assertFalse(opt.isDexoptIdleBackgroundJob());
@@ -67,7 +66,6 @@
                 DexoptOptions.DEXOPT_BOOT_COMPLETE |
                 DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES |
                 DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX |
-                DexoptOptions.DEXOPT_ONLY_SHARED_DEX |
                 DexoptOptions.DEXOPT_DOWNGRADE  |
                 DexoptOptions.DEXOPT_AS_SHARED_LIBRARY |
                 DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB |
@@ -81,7 +79,6 @@
         assertTrue(opt.isBootComplete());
         assertTrue(opt.isCheckForProfileUpdates());
         assertTrue(opt.isDexoptOnlySecondaryDex());
-        assertTrue(opt.isDexoptOnlySharedDex());
         assertTrue(opt.isDowngrade());
         assertTrue(opt.isForce());
         assertTrue(opt.isDexoptAsSharedLibrary());
@@ -113,7 +110,6 @@
             assertTrue(opt.isBootComplete());
             assertTrue(opt.isCheckForProfileUpdates());
             assertFalse(opt.isDexoptOnlySecondaryDex());
-            assertFalse(opt.isDexoptOnlySharedDex());
             assertFalse(opt.isDowngrade());
             assertTrue(opt.isForce());
             assertFalse(opt.isDexoptAsSharedLibrary());
@@ -131,7 +127,6 @@
         assertTrue(opt.isBootComplete());
         assertFalse(opt.isCheckForProfileUpdates());
         assertFalse(opt.isDexoptOnlySecondaryDex());
-        assertFalse(opt.isDexoptOnlySharedDex());
         assertFalse(opt.isDowngrade());
         assertTrue(opt.isForce());
         assertFalse(opt.isDexoptAsSharedLibrary());
diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
index 91c2fe0..8e81e2d 100644
--- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
@@ -1371,6 +1371,39 @@
         verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
     }
 
+    @Test
+    public void dreamWhenDocked_ambientModeSuppressed_suppressionEnabled() {
+        mUiManagerService.setStartDreamImmediatelyOnDock(true);
+        mUiManagerService.setDreamsDisabledByAmbientModeSuppression(true);
+
+        when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(true);
+        triggerDockIntent();
+        verifyAndSendResultBroadcast();
+        verify(mInjector, never()).startDreamWhenDockedIfAppropriate(mContext);
+    }
+
+    @Test
+    public void dreamWhenDocked_ambientModeSuppressed_suppressionDisabled() {
+        mUiManagerService.setStartDreamImmediatelyOnDock(true);
+        mUiManagerService.setDreamsDisabledByAmbientModeSuppression(false);
+
+        when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(true);
+        triggerDockIntent();
+        verifyAndSendResultBroadcast();
+        verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
+    }
+
+    @Test
+    public void dreamWhenDocked_ambientModeNotSuppressed_suppressionEnabled() {
+        mUiManagerService.setStartDreamImmediatelyOnDock(true);
+        mUiManagerService.setDreamsDisabledByAmbientModeSuppression(true);
+
+        when(mLocalPowerManager.isAmbientDisplaySuppressed()).thenReturn(false);
+        triggerDockIntent();
+        verifyAndSendResultBroadcast();
+        verify(mInjector).startDreamWhenDockedIfAppropriate(mContext);
+    }
+
     private void triggerDockIntent() {
         final Intent dockedIntent =
                 new Intent(Intent.ACTION_DOCK_EVENT)
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
index 1e94577..248a3fc 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.notification;
 
+import static android.content.pm.PackageManager.MATCH_ANY_USER;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING;
@@ -30,9 +31,11 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.intThat;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -49,6 +52,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
 import android.content.pm.VersionedPackage;
+import android.content.res.Resources;
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.service.notification.NotificationListenerFilter;
@@ -69,6 +73,7 @@
 
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentMatcher;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.internal.util.reflection.FieldSetter;
@@ -77,6 +82,7 @@
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
 import java.util.List;
 
 public class NotificationListenersTest extends UiServiceTestCase {
@@ -85,6 +91,8 @@
     private PackageManager mPm;
     @Mock
     private IPackageManager miPm;
+    @Mock
+    private Resources mResources;
 
     @Mock
     NotificationManagerService mNm;
@@ -96,7 +104,8 @@
 
     private ComponentName mCn1 = new ComponentName("pkg", "pkg.cmp");
     private ComponentName mCn2 = new ComponentName("pkg2", "pkg2.cmp2");
-
+    private ComponentName mUninstalledComponent = new ComponentName("pkg3",
+            "pkg3.NotificationListenerService");
 
     @Before
     public void setUp() throws Exception {
@@ -111,7 +120,7 @@
 
     @Test
     public void testReadExtraTag() throws Exception {
-        String xml = "<" + TAG_REQUESTED_LISTENERS+ ">"
+        String xml = "<" + TAG_REQUESTED_LISTENERS + ">"
                 + "<listener component=\"" + mCn1.flattenToString() + "\" user=\"0\">"
                 + "<allowed types=\"7\" />"
                 + "</listener>"
@@ -131,11 +140,55 @@
     }
 
     @Test
+    public void loadDefaultsFromConfig_forHeadlessSystemUser_loadUninstalled() throws Exception {
+        // setup with headless system user mode
+        mListeners = spy(mNm.new NotificationListeners(
+                mContext, new Object(), mock(ManagedServices.UserProfiles.class), miPm,
+                /* isHeadlessSystemUserMode= */ true));
+        mockDefaultListenerConfigForUninstalledComponent(mUninstalledComponent);
+
+        mListeners.loadDefaultsFromConfig();
+
+        assertThat(mListeners.getDefaultComponents()).contains(mUninstalledComponent);
+    }
+
+    @Test
+    public void loadDefaultsFromConfig_forNonHeadlessSystemUser_ignoreUninstalled()
+            throws Exception {
+        // setup without headless system user mode
+        mListeners = spy(mNm.new NotificationListeners(
+                mContext, new Object(), mock(ManagedServices.UserProfiles.class), miPm,
+                /* isHeadlessSystemUserMode= */ false));
+        mockDefaultListenerConfigForUninstalledComponent(mUninstalledComponent);
+
+        mListeners.loadDefaultsFromConfig();
+
+        assertThat(mListeners.getDefaultComponents()).doesNotContain(mUninstalledComponent);
+    }
+
+    private void mockDefaultListenerConfigForUninstalledComponent(ComponentName componentName) {
+        ArraySet<ComponentName> components = new ArraySet<>(Arrays.asList(componentName));
+        when(mResources
+                .getString(
+                        com.android.internal.R.string.config_defaultListenerAccessPackages))
+                .thenReturn(componentName.getPackageName());
+        when(mContext.getResources()).thenReturn(mResources);
+        doReturn(components).when(mListeners).queryPackageForServices(
+                eq(componentName.getPackageName()),
+                intThat(hasIntBitFlag(MATCH_ANY_USER)),
+                anyInt());
+    }
+
+    public static ArgumentMatcher<Integer> hasIntBitFlag(int flag) {
+        return arg -> arg != null && ((arg & flag) == flag);
+    }
+
+    @Test
     public void testWriteExtraTag() throws Exception {
         NotificationListenerFilter nlf = new NotificationListenerFilter(7, new ArraySet<>());
         VersionedPackage a1 = new VersionedPackage("pkg1", 243);
         NotificationListenerFilter nlf2 =
-                new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[] {a1}));
+                new NotificationListenerFilter(4, new ArraySet<>(new VersionedPackage[]{a1}));
         mListeners.setNotificationListenerFilter(Pair.create(mCn1, 0), nlf);
         mListeners.setNotificationListenerFilter(Pair.create(mCn2, 10), nlf2);
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index d5e336b..eed32d7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -40,14 +40,18 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.timeout;
 
+import android.app.ActivityOptions;
 import android.app.WaitResult;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
+import android.os.Binder;
 import android.os.ConditionVariable;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
 import android.view.Display;
@@ -308,4 +312,40 @@
         waitHandlerIdle(mAtm.mH);
         verify(mRootWindowContainer, timeout(TIMEOUT_MS)).startHomeOnEmptyDisplays("userUnlocked");
     }
+
+    /** Verifies that launch from recents sets the launch cookie on the activity. */
+    @Test
+    public void testStartActivityFromRecents_withLaunchCookie() {
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+
+        IBinder launchCookie = new Binder("test_launch_cookie");
+        ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchCookie(launchCookie);
+        SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle(options.toBundle());
+
+        doNothing().when(mSupervisor.mService).moveTaskToFrontLocked(eq(null), eq(null), anyInt(),
+                anyInt(), any());
+
+        mSupervisor.startActivityFromRecents(-1, -1, activity.getRootTaskId(), safeOptions);
+
+        assertThat(activity.mLaunchCookie).isEqualTo(launchCookie);
+        verify(mAtm).moveTaskToFrontLocked(any(), eq(null), anyInt(), anyInt(), eq(safeOptions));
+    }
+
+    /** Verifies that launch from recents doesn't set the launch cookie on the activity. */
+    @Test
+    public void testStartActivityFromRecents_withoutLaunchCookie() {
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+
+        SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle(
+                ActivityOptions.makeBasic().toBundle());
+
+        doNothing().when(mSupervisor.mService).moveTaskToFrontLocked(eq(null), eq(null), anyInt(),
+                anyInt(), any());
+
+        mSupervisor.startActivityFromRecents(-1, -1, activity.getRootTaskId(), safeOptions);
+
+        assertThat(activity.mLaunchCookie).isNull();
+        verify(mAtm).moveTaskToFrontLocked(any(), eq(null), anyInt(), anyInt(), eq(safeOptions));
+    }
 }
diff --git a/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml b/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml
index 5a135c9..7fe4bae 100644
--- a/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml
+++ b/tests/RollbackTest/SampleRollbackApp/AndroidManifest.xml
@@ -16,7 +16,7 @@
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.sample.rollbackapp" >
-    <uses-permission android:name="android.permission.TEST_MANAGE_ROLLBACKS" />
+    <uses-permission android:name="android.permission.MANAGE_ROLLBACKS" />
     <application
         android:label="@string/title_activity_main">
         <activity
@@ -28,4 +28,4 @@
             </intent-filter>
         </activity>
     </application>
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
index 68a450d..1b0f035 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
@@ -26,6 +26,7 @@
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
 import com.android.tools.lint.detector.api.getUMethod
+import com.google.android.lint.aidl.hasPermissionMethodAnnotation
 import com.intellij.psi.PsiType
 import org.jetbrains.uast.UAnnotation
 import org.jetbrains.uast.UBlockExpression
@@ -149,11 +150,6 @@
             enabledByDefault = false
         )
 
-        private fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations
-            .any {
-                it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD)
-            }
-
         private fun isPermissionMethodReturnType(method: UMethod): Boolean =
             listOf(PsiType.VOID, PsiType.INT, PsiType.BOOLEAN).contains(method.returnType)
 
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt
index 5106111..d120e1d 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt
@@ -18,37 +18,54 @@
 
 import com.android.tools.lint.detector.api.JavaContext
 import com.android.tools.lint.detector.api.Location
-import com.intellij.psi.PsiVariable
+import com.android.tools.lint.detector.api.getUMethod
+import org.jetbrains.kotlin.psi.psiUtil.parameterIndex
 import org.jetbrains.uast.UCallExpression
-import org.jetbrains.uast.ULiteralExpression
-import org.jetbrains.uast.UQualifiedReferenceExpression
-import org.jetbrains.uast.USimpleNameReferenceExpression
-import org.jetbrains.uast.asRecursiveLogString
+import org.jetbrains.uast.evaluateString
+import org.jetbrains.uast.visitor.AbstractUastVisitor
 
 /**
- * Helper ADT class that facilitates the creation of lint auto fixes
+ * Helper class that facilitates the creation of lint auto fixes
  *
  * Handles "Single" permission checks that should be migrated to @EnforcePermission(...), as well as consecutive checks
  * that should be migrated to @EnforcePermission(allOf={...})
  *
  * TODO: handle anyOf style annotations
  */
-sealed class EnforcePermissionFix {
-    abstract fun locations(): List<Location>
-    abstract fun javaAnnotationParameter(): String
-
-    fun javaAnnotation(): String = "@$ANNOTATION_ENFORCE_PERMISSION(${javaAnnotationParameter()})"
+data class EnforcePermissionFix(
+    val locations: List<Location>,
+    val permissionNames: List<String>
+) {
+    val annotation: String
+        get() {
+            val quotedPermissions = permissionNames.joinToString(", ") { """"$it"""" }
+            val annotationParameter =
+                if (permissionNames.size > 1) "allOf={$quotedPermissions}" else quotedPermissions
+            return "@$ANNOTATION_ENFORCE_PERMISSION($annotationParameter)"
+        }
 
     companion object {
-        fun fromCallExpression(callExpression: UCallExpression, context: JavaContext): SingleFix =
-            SingleFix(
-                getPermissionCheckLocation(context, callExpression),
-                getPermissionCheckArgumentValue(callExpression)
-            )
+        /**
+         * conditionally constructs EnforcePermissionFix from a UCallExpression
+         * @return EnforcePermissionFix if the called method is annotated with @PermissionMethod, else null
+         */
+        fun fromCallExpression(
+            context: JavaContext,
+            callExpression: UCallExpression
+        ): EnforcePermissionFix? =
+            if (isPermissionMethodCall(callExpression)) {
+                EnforcePermissionFix(
+                    listOf(getPermissionCheckLocation(context, callExpression)),
+                    getPermissionCheckValues(callExpression)
+                )
+            } else null
 
-        fun maybeAddManifestPrefix(permissionName: String): String =
-            if (permissionName.contains(".")) permissionName
-            else "android.Manifest.permission.$permissionName"
+
+        fun compose(individuals: List<EnforcePermissionFix>): EnforcePermissionFix =
+            EnforcePermissionFix(
+                individuals.flatMap { it.locations },
+                individuals.flatMap { it.permissionNames }
+            )
 
         /**
          * Given a permission check, get its proper location
@@ -70,49 +87,51 @@
         }
 
         /**
-         * Given a permission check and an argument,
-         * pull out the permission value that is being used
+         * Given a @PermissionMethod, find arguments annotated with @PermissionName
+         * and pull out the permission value(s) being used.  Also evaluates nested calls
+         * to @PermissionMethod(s) in the given method's body.
          */
-        private fun getPermissionCheckArgumentValue(
-            callExpression: UCallExpression,
-            argumentPosition: Int = 0
-        ): String {
+        private fun getPermissionCheckValues(
+            callExpression: UCallExpression
+        ): List<String> {
+            if (!isPermissionMethodCall(callExpression)) return emptyList()
 
-            val identifier = when (
-                val argument = callExpression.valueArguments.getOrNull(argumentPosition)
-            ) {
-                is UQualifiedReferenceExpression -> when (val selector = argument.selector) {
-                    is USimpleNameReferenceExpression ->
-                        ((selector.resolve() as PsiVariable).computeConstantValue() as String)
+            val result = mutableSetOf<String>() // protect against duplicate permission values
+            val visitedCalls = mutableSetOf<UCallExpression>() // don't visit the same call twice
+            val bfsQueue = ArrayDeque(listOf(callExpression))
 
-                    else -> throw RuntimeException(
-                        "Couldn't resolve argument: ${selector.asRecursiveLogString()}"
-                    )
-                }
+            // Breadth First Search - evalutaing nested @PermissionMethod(s) in the available
+            // source code for @PermissionName(s).
+            while (bfsQueue.isNotEmpty()) {
+                val current = bfsQueue.removeFirst()
+                visitedCalls.add(current)
+                result.addAll(findPermissions(current))
 
-                is USimpleNameReferenceExpression -> (
-                        (argument.resolve() as PsiVariable).computeConstantValue() as String)
-
-                is ULiteralExpression -> argument.value as String
-
-                else -> throw RuntimeException(
-                    "Couldn't resolve argument: ${argument?.asRecursiveLogString()}"
-                )
+                current.resolve()?.getUMethod()?.accept(object : AbstractUastVisitor() {
+                    override fun visitCallExpression(node: UCallExpression): Boolean {
+                        if (isPermissionMethodCall(node) && node !in visitedCalls) {
+                            bfsQueue.add(node)
+                        }
+                        return false
+                    }
+                })
             }
 
-            return identifier.substringAfterLast(".")
+            return result.toList()
+        }
+
+        private fun findPermissions(
+            callExpression: UCallExpression,
+        ): List<String> {
+            val indices = callExpression.resolve()?.getUMethod()
+                ?.uastParameters
+                ?.filter(::hasPermissionNameAnnotation)
+                ?.mapNotNull { it.sourcePsi?.parameterIndex() }
+                ?: emptyList()
+
+            return indices.mapNotNull {
+                callExpression.getArgumentForParameter(it)?.evaluateString()
+            }
         }
     }
 }
-
-data class SingleFix(val location: Location, val permissionName: String) : EnforcePermissionFix() {
-    override fun locations(): List<Location> = listOf(this.location)
-    override fun javaAnnotationParameter(): String = maybeAddManifestPrefix(this.permissionName)
-}
-data class AllOfFix(val checks: List<SingleFix>) : EnforcePermissionFix() {
-    override fun locations(): List<Location> = this.checks.map { it.location }
-    override fun javaAnnotationParameter(): String =
-        "allOf={${
-            this.checks.joinToString(", ") { maybeAddManifestPrefix(it.permissionName) }
-        }}"
-}
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
new file mode 100644
index 0000000..edbdd8d
--- /dev/null
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.google.android.lint.aidl
+
+import com.android.tools.lint.detector.api.getUMethod
+import com.google.android.lint.ANNOTATION_PERMISSION_METHOD
+import com.google.android.lint.ANNOTATION_PERMISSION_NAME
+import com.google.android.lint.CLASS_STUB
+import com.intellij.psi.PsiAnonymousClass
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UParameter
+
+/**
+ * Given a UMethod, determine if this method is
+ * an entrypoint to an interface generated by AIDL,
+ * returning the interface name if so
+ */
+fun getContainingAidlInterface(node: UMethod): String? {
+    if (!isInClassCalledStub(node)) return null
+    for (superMethod in node.findSuperMethods()) {
+        for (extendsInterface in superMethod.containingClass?.extendsList?.referenceElements
+            ?: continue) {
+            if (extendsInterface.qualifiedName == IINTERFACE_INTERFACE) {
+                return superMethod.containingClass?.name
+            }
+        }
+    }
+    return null
+}
+
+private fun isInClassCalledStub(node: UMethod): Boolean {
+    (node.containingClass as? PsiAnonymousClass)?.let {
+        return it.baseClassReference.referenceName == CLASS_STUB
+    }
+    return node.containingClass?.extendsList?.referenceElements?.any {
+        it.referenceName == CLASS_STUB
+    } ?: false
+}
+
+fun isPermissionMethodCall(callExpression: UCallExpression): Boolean {
+    val method = callExpression.resolve()?.getUMethod() ?: return false
+    return hasPermissionMethodAnnotation(method)
+}
+
+fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations
+    .any {
+        it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD)
+    }
+
+fun hasPermissionNameAnnotation(parameter: UParameter) = parameter.annotations.any {
+    it.hasQualifiedName(ANNOTATION_PERMISSION_NAME)
+}
diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt
index 2cea394..2c53f39 100644
--- a/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt
+++ b/tools/lint/checks/src/main/java/com/google/android/lint/aidl/ManualPermissionCheckDetector.kt
@@ -25,9 +25,6 @@
 import com.android.tools.lint.detector.api.Scope
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
-import com.google.android.lint.CLASS_STUB
-import com.google.android.lint.ENFORCE_PERMISSION_METHODS
-import com.intellij.psi.PsiAnonymousClass
 import org.jetbrains.uast.UBlockExpression
 import org.jetbrains.uast.UCallExpression
 import org.jetbrains.uast.UElement
@@ -56,7 +53,7 @@
             val body = (node.uastBody as? UBlockExpression) ?: return
             val fix = accumulateSimplePermissionCheckFixes(body) ?: return
 
-            val javaRemoveFixes = fix.locations().map {
+            val javaRemoveFixes = fix.locations.map {
                 fix()
                     .replace()
                     .reformat(true)
@@ -67,7 +64,7 @@
             }
 
             val javaAnnotateFix = fix()
-                .annotate(fix.javaAnnotation())
+                .annotate(fix.annotation)
                 .range(context.getLocation(node))
                 .autoFix()
                 .build()
@@ -77,7 +74,7 @@
 
             context.report(
                 ISSUE_USE_ENFORCE_PERMISSION_ANNOTATION,
-                fix.locations().last(),
+                fix.locations.last(),
                 message,
                 fix().composite(*javaRemoveFixes.toTypedArray(), javaAnnotateFix)
             )
@@ -97,14 +94,14 @@
          */
         private fun accumulateSimplePermissionCheckFixes(methodBody: UBlockExpression):
                 EnforcePermissionFix? {
-            val singleFixes = mutableListOf<SingleFix>()
+            val singleFixes = mutableListOf<EnforcePermissionFix>()
             for (expression in methodBody.expressions) {
                 singleFixes.add(getPermissionCheckFix(expression) ?: break)
             }
             return when (singleFixes.size) {
                 0 -> null
                 1 -> singleFixes[0]
-                else -> AllOfFix(singleFixes)
+                else -> EnforcePermissionFix.compose(singleFixes)
             }
         }
 
@@ -113,7 +110,7 @@
          * the helper for creating a lint auto fix to @EnforcePermission
          */
         private fun getPermissionCheckFix(startingExpression: UElement?):
-                SingleFix? {
+                EnforcePermissionFix? {
             return when (startingExpression) {
                 is UQualifiedReferenceExpression -> getPermissionCheckFix(
                     startingExpression.selector
@@ -121,11 +118,8 @@
 
                 is UIfExpression -> getPermissionCheckFix(startingExpression.condition)
 
-                is UCallExpression -> {
-                    return if (isPermissionCheck(startingExpression))
-                        EnforcePermissionFix.fromCallExpression(startingExpression, context)
-                    else null
-                }
+                is UCallExpression -> return EnforcePermissionFix
+                            .fromCallExpression(context, startingExpression)
 
                 else -> null
             }
@@ -160,40 +154,5 @@
             ),
             enabledByDefault = false, // TODO: enable once b/241171714 is resolved
         )
-
-        private fun isPermissionCheck(callExpression: UCallExpression): Boolean {
-            val method = callExpression.resolve() ?: return false
-            val className = method.containingClass?.qualifiedName
-            return ENFORCE_PERMISSION_METHODS.any {
-                it.clazz == className && it.name == method.name
-            }
-        }
-
-        /**
-         * given a UMethod, determine if this method is
-         * an entrypoint to an interface generated by AIDL,
-         * returning the interface name if so
-         */
-        fun getContainingAidlInterface(node: UMethod): String? {
-            if (!isInClassCalledStub(node)) return null
-            for (superMethod in node.findSuperMethods()) {
-                for (extendsInterface in superMethod.containingClass?.extendsList?.referenceElements
-                    ?: continue) {
-                    if (extendsInterface.qualifiedName == IINTERFACE_INTERFACE) {
-                        return superMethod.containingClass?.name
-                    }
-                }
-            }
-            return null
-        }
-
-        private fun isInClassCalledStub(node: UMethod): Boolean {
-            (node.containingClass as? PsiAnonymousClass)?.let {
-                return it.baseClassReference.referenceName == CLASS_STUB
-            }
-            return node.containingClass?.extendsList?.referenceElements?.any {
-                it.referenceName == CLASS_STUB
-            } ?: false
-        }
     }
 }
diff --git a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt
index 1a1c6bc..a968f5e 100644
--- a/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt
+++ b/tools/lint/checks/src/test/java/com/google/android/lint/aidl/ManualPermissionCheckDetectorTest.kt
@@ -19,6 +19,7 @@
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFile
 import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.checks.infrastructure.TestMode
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 
@@ -42,7 +43,7 @@
                         private Context mContext;
                         @Override
                         public void test() throws android.os.RemoteException {
-                            mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo");
+                            mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
                         }
                     }
                 """
@@ -53,8 +54,8 @@
             .expect(
                 """
                 src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
-                        mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo");
-                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                 0 errors, 1 warnings
                 """
             )
@@ -62,9 +63,9 @@
                 """
                 Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
                 @@ -5 +5
-                +     @android.annotation.EnforcePermission(android.Manifest.permission.READ_CONTACTS)
+                +     @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
                 @@ -7 +8
-                -         mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo");
+                -         mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
                 """
             )
     }
@@ -81,7 +82,7 @@
                             @Override
                             public void test() throws android.os.RemoteException {
                                 mContext.enforceCallingOrSelfPermission(
-                                    "android.Manifest.permission.READ_CONTACTS", "foo");
+                                    "android.permission.READ_CONTACTS", "foo");
                             }
                         };
                     }
@@ -102,10 +103,49 @@
                 """
                 Fix for src/Foo.java line 8: Annotate with @EnforcePermission:
                 @@ -6 +6
-                +         @android.annotation.EnforcePermission(android.Manifest.permission.READ_CONTACTS)
+                +         @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
                 @@ -8 +9
                 -             mContext.enforceCallingOrSelfPermission(
-                -                 "android.Manifest.permission.READ_CONTACTS", "foo");
+                -                 "android.permission.READ_CONTACTS", "foo");
+                """
+            )
+    }
+
+    fun testConstantEvaluation() {
+        lint().files(
+            java(
+                """
+                    import android.content.Context;
+                    import android.test.ITest;
+
+                    public class Foo extends ITest.Stub {
+                        private Context mContext;
+                        @Override
+                        public void test() throws android.os.RemoteException {
+                            mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo");
+                        }
+                    }
+                """
+            ).indented(),
+            *stubs,
+            manifestStub
+        )
+            .run()
+            .expect(
+                """
+                src/Foo.java:8: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+                        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo");
+                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+                Fix for src/Foo.java line 7: Annotate with @EnforcePermission:
+                @@ -6 +6
+                +     @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
+                @@ -8 +9
+                -         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo");
                 """
             )
     }
@@ -122,9 +162,9 @@
                             @Override
                             public void test() throws android.os.RemoteException {
                                 mContext.enforceCallingOrSelfPermission(
-                                    "android.Manifest.permission.READ_CONTACTS", "foo");
+                                    "android.permission.READ_CONTACTS", "foo");
                                 mContext.enforceCallingOrSelfPermission(
-                                    "android.Manifest.permission.WRITE_CONTACTS", "foo");
+                                    "android.permission.WRITE_CONTACTS", "foo");
                             }
                         };
                     }
@@ -144,13 +184,13 @@
             .expectFixDiffs(
                 """
                 Fix for src/Foo.java line 10: Annotate with @EnforcePermission:
-                @@ -6 +6                                                                                                                                                                                                       
-                +         @android.annotation.EnforcePermission(allOf={android.Manifest.permission.READ_CONTACTS, android.Manifest.permission.WRITE_CONTACTS})
+                @@ -6 +6
+                +         @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"})
                 @@ -8 +9
                 -             mContext.enforceCallingOrSelfPermission(
-                -                 "android.Manifest.permission.READ_CONTACTS", "foo");
+                -                 "android.permission.READ_CONTACTS", "foo");
                 -             mContext.enforceCallingOrSelfPermission(
-                -                 "android.Manifest.permission.WRITE_CONTACTS", "foo");
+                -                 "android.permission.WRITE_CONTACTS", "foo");
                 """
             )
     }
@@ -166,7 +206,7 @@
                         @Override
                         public void test() throws android.os.RemoteException {
                             long uid = Binder.getCallingUid();
-                            mContext.enforceCallingOrSelfPermission("android.Manifest.permission.READ_CONTACTS", "foo");
+                            mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
                         }
                     }
                 """
@@ -177,6 +217,149 @@
             .expectClean()
     }
 
+    fun testPermissionHelper() {
+        lint().skipTestModes(TestMode.PARENTHESIZED).files(
+            java(
+                """
+                    import android.content.Context;
+                    import android.test.ITest;
+
+                    public class Foo extends ITest.Stub {
+                        private Context mContext;
+
+                        @android.content.pm.PermissionMethod
+                        private void helper() {
+                            mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+                        }
+
+                        @Override
+                        public void test() throws android.os.RemoteException {
+                            helper();
+                        }
+                    }
+                """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                src/Foo.java:14: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+                        helper();
+                        ~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+                Fix for src/Foo.java line 14: Annotate with @EnforcePermission:
+                @@ -12 +12
+                +     @android.annotation.EnforcePermission("android.permission.READ_CONTACTS")
+                @@ -14 +15
+                -         helper();
+                """
+            )
+    }
+
+    fun testPermissionHelperAllOf() {
+        lint().skipTestModes(TestMode.PARENTHESIZED).files(
+            java(
+                """
+                import android.content.Context;
+                import android.test.ITest;
+
+                public class Foo extends ITest.Stub {
+                    private Context mContext;
+
+                    @android.content.pm.PermissionMethod
+                    private void helper() {
+                        mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+                        mContext.enforceCallingOrSelfPermission("android.permission.WRITE_CONTACTS", "foo");
+                    }
+
+                    @Override
+                    public void test() throws android.os.RemoteException {
+                        helper();
+                        mContext.enforceCallingOrSelfPermission("FOO", "foo");
+                    }
+                }
+                """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                src/Foo.java:16: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+                        mContext.enforceCallingOrSelfPermission("FOO", "foo");
+                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+                Fix for src/Foo.java line 16: Annotate with @EnforcePermission:
+                @@ -13 +13
+                +     @android.annotation.EnforcePermission(allOf={"android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS", "FOO"})
+                @@ -15 +16
+                -         helper();
+                -         mContext.enforceCallingOrSelfPermission("FOO", "foo");
+                """
+            )
+    }
+
+
+    fun testPermissionHelperNested() {
+        lint().skipTestModes(TestMode.PARENTHESIZED).files(
+            java(
+                """
+                import android.content.Context;
+                import android.test.ITest;
+
+                public class Foo extends ITest.Stub {
+                    private Context mContext;
+
+                    @android.content.pm.PermissionMethod
+                    private void helperHelper() {
+                        helper("android.permission.WRITE_CONTACTS");
+                    }
+
+                    @android.content.pm.PermissionMethod
+                    private void helper(@android.content.pm.PermissionName String extraPermission) {
+                        mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo");
+                    }
+
+                    @Override
+                    public void test() throws android.os.RemoteException {
+                        helperHelper();
+                    }
+                }
+                """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                src/Foo.java:19: Warning: ITest permission check can be converted to @EnforcePermission annotation [UseEnforcePermissionAnnotation]
+                        helperHelper();
+                        ~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+                Fix for src/Foo.java line 19: Annotate with @EnforcePermission:
+                @@ -17 +17
+                +     @android.annotation.EnforcePermission(allOf={"android.permission.WRITE_CONTACTS", "android.permission.READ_CONTACTS"})
+                @@ -19 +20
+                -         helperHelper();
+                """
+            )
+    }
+
+
+
     companion object {
         private val aidlStub: TestFile = java(
             """
@@ -192,7 +375,8 @@
             """
                 package android.content;
                 public class Context {
-                    public void enforceCallingOrSelfPermission(String permission, String message) {}
+                    @android.content.pm.PermissionMethod
+                    public void enforceCallingOrSelfPermission(@android.content.pm.PermissionName String permission, String message) {}
                 }
             """
         ).indented()
@@ -206,6 +390,59 @@
             """
         ).indented()
 
-        val stubs = arrayOf(aidlStub, contextStub, binderStub)
+        private val permissionMethodStub: TestFile = java(
+            """
+                package android.content.pm;
+
+                import static java.lang.annotation.ElementType.METHOD;
+                import static java.lang.annotation.RetentionPolicy.CLASS;
+
+                import java.lang.annotation.Retention;
+                import java.lang.annotation.Target;
+
+                @Retention(CLASS)
+                @Target({METHOD})
+                public @interface PermissionMethod {}
+            """
+        ).indented()
+
+        private val permissionNameStub: TestFile = java(
+            """
+                package android.content.pm;
+
+                import static java.lang.annotation.ElementType.FIELD;
+                import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+                import static java.lang.annotation.ElementType.METHOD;
+                import static java.lang.annotation.ElementType.PARAMETER;
+                import static java.lang.annotation.RetentionPolicy.CLASS;
+
+                import java.lang.annotation.Retention;
+                import java.lang.annotation.Target;
+
+                @Retention(CLASS)
+                @Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD})
+                public @interface PermissionName {}
+            """
+        ).indented()
+
+        private val manifestStub: TestFile = java(
+            """
+                package android;
+
+                public final class Manifest {
+                    public static final class permission {
+                        public static final String READ_CONTACTS="android.permission.READ_CONTACTS";
+                    }
+                }
+            """.trimIndent()
+        )
+
+        val stubs = arrayOf(
+            aidlStub,
+            contextStub,
+            binderStub,
+            permissionMethodStub,
+            permissionNameStub
+        )
     }
 }