Merge "Merge IInputMethodManagerInvoker and IInputMethodManagerGlobalInvoker"
diff --git a/apct-tests/perftests/core/Android.bp b/apct-tests/perftests/core/Android.bp
index ab20fdb..98e4f45 100644
--- a/apct-tests/perftests/core/Android.bp
+++ b/apct-tests/perftests/core/Android.bp
@@ -44,6 +44,8 @@
         "apct-perftests-utils",
         "collector-device-lib",
         "compatibility-device-util-axt",
+        "junit",
+        "junit-params",
         "core-tests-support",
         "guava",
     ],
diff --git a/apct-tests/perftests/core/src/android/libcore/DeepArrayOpsPerfTest.java b/apct-tests/perftests/core/src/android/libcore/DeepArrayOpsPerfTest.java
index 3f4f6af..3ebaa4c 100644
--- a/apct-tests/perftests/core/src/android/libcore/DeepArrayOpsPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/DeepArrayOpsPerfTest.java
@@ -20,18 +20,19 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
-import org.junit.Before;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
 
 import java.lang.reflect.Array;
 import java.lang.reflect.Constructor;
 import java.util.Arrays;
 import java.util.Collection;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class DeepArrayOpsPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
@@ -39,19 +40,14 @@
     private Object[] mArray;
     private Object[] mArray2;
 
-    @Parameterized.Parameter(0)
-    public int mArrayLength;
-
-    @Parameterized.Parameters(name = "mArrayLength({0})")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(new Object[][] {{1}, {4}, {16}, {32}, {2048}});
     }
 
-    @Before
-    public void setUp() throws Exception {
-        mArray = new Object[mArrayLength * 14];
-        mArray2 = new Object[mArrayLength * 14];
-        for (int i = 0; i < mArrayLength; i += 14) {
+    public void setUp(int arrayLength) throws Exception {
+        mArray = new Object[arrayLength * 14];
+        mArray2 = new Object[arrayLength * 14];
+        for (int i = 0; i < arrayLength; i += 14) {
             mArray[i] = new IntWrapper(i);
             mArray2[i] = new IntWrapper(i);
 
@@ -99,7 +95,9 @@
     }
 
     @Test
-    public void deepHashCode() {
+    @Parameters(method = "getData")
+    public void deepHashCode(int arrayLength) throws Exception {
+        setUp(arrayLength);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             Arrays.deepHashCode(mArray);
@@ -107,7 +105,9 @@
     }
 
     @Test
-    public void deepEquals() {
+    @Parameters(method = "getData")
+    public void deepEquals(int arrayLength) throws Exception {
+        setUp(arrayLength);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             Arrays.deepEquals(mArray, mArray2);
diff --git a/apct-tests/perftests/core/src/android/libcore/SystemArrayCopyPerfTest.java b/apct-tests/perftests/core/src/android/libcore/SystemArrayCopyPerfTest.java
index 5aacfc2..20f1309 100644
--- a/apct-tests/perftests/core/src/android/libcore/SystemArrayCopyPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/SystemArrayCopyPerfTest.java
@@ -20,22 +20,22 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.util.Arrays;
 import java.util.Collection;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class SystemArrayCopyPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameters(name = "arrayLength={0}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(
                 new Object[][] {
                     {2}, {4}, {8}, {16}, {32}, {64}, {128}, {256}, {512}, {1024}, {2048}, {4096},
@@ -43,12 +43,10 @@
                 });
     }
 
-    @Parameterized.Parameter(0)
-    public int arrayLength;
-
     // Provides benchmarking for different types of arrays using the arraycopy function.
     @Test
-    public void timeSystemCharArrayCopy() {
+    @Parameters(method = "getData")
+    public void timeSystemCharArrayCopy(int arrayLength) {
         final int len = arrayLength;
         char[] src = new char[len];
         char[] dst = new char[len];
@@ -59,7 +57,8 @@
     }
 
     @Test
-    public void timeSystemByteArrayCopy() {
+    @Parameters(method = "getData")
+    public void timeSystemByteArrayCopy(int arrayLength) {
         final int len = arrayLength;
         byte[] src = new byte[len];
         byte[] dst = new byte[len];
@@ -70,7 +69,8 @@
     }
 
     @Test
-    public void timeSystemShortArrayCopy() {
+    @Parameters(method = "getData")
+    public void timeSystemShortArrayCopy(int arrayLength) {
         final int len = arrayLength;
         short[] src = new short[len];
         short[] dst = new short[len];
@@ -81,7 +81,8 @@
     }
 
     @Test
-    public void timeSystemIntArrayCopy() {
+    @Parameters(method = "getData")
+    public void timeSystemIntArrayCopy(int arrayLength) {
         final int len = arrayLength;
         int[] src = new int[len];
         int[] dst = new int[len];
@@ -92,7 +93,8 @@
     }
 
     @Test
-    public void timeSystemLongArrayCopy() {
+    @Parameters(method = "getData")
+    public void timeSystemLongArrayCopy(int arrayLength) {
         final int len = arrayLength;
         long[] src = new long[len];
         long[] dst = new long[len];
@@ -103,7 +105,8 @@
     }
 
     @Test
-    public void timeSystemFloatArrayCopy() {
+    @Parameters(method = "getData")
+    public void timeSystemFloatArrayCopy(int arrayLength) {
         final int len = arrayLength;
         float[] src = new float[len];
         float[] dst = new float[len];
@@ -114,7 +117,8 @@
     }
 
     @Test
-    public void timeSystemDoubleArrayCopy() {
+    @Parameters(method = "getData")
+    public void timeSystemDoubleArrayCopy(int arrayLength) {
         final int len = arrayLength;
         double[] src = new double[len];
         double[] dst = new double[len];
@@ -125,7 +129,8 @@
     }
 
     @Test
-    public void timeSystemBooleanArrayCopy() {
+    @Parameters(method = "getData")
+    public void timeSystemBooleanArrayCopy(int arrayLength) {
         final int len = arrayLength;
         boolean[] src = new boolean[len];
         boolean[] dst = new boolean[len];
diff --git a/apct-tests/perftests/core/src/android/libcore/XmlSerializePerfTest.java b/apct-tests/perftests/core/src/android/libcore/XmlSerializePerfTest.java
index eec0734..b1b594d 100644
--- a/apct-tests/perftests/core/src/android/libcore/XmlSerializePerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/XmlSerializePerfTest.java
@@ -20,42 +20,32 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
-import org.junit.Before;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 import org.xmlpull.v1.XmlSerializer;
 
 import java.io.CharArrayWriter;
 import java.lang.reflect.Constructor;
-import java.util.Arrays;
-import java.util.Collection;
 import java.util.Random;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class XmlSerializePerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameters(name = "mDatasetAsString({0}), mSeed({1})")
-    public static Collection<Object[]> data() {
-        return Arrays.asList(
-                new Object[][] {
-                    {"0.99 0.7 0.7 0.7 0.7 0.7", 854328},
-                    {"0.999 0.3 0.3 0.95 0.9 0.9", 854328},
-                    {"0.99 0.7 0.7 0.7 0.7 0.7", 312547},
-                    {"0.999 0.3 0.3 0.95 0.9 0.9", 312547}
-                });
+    private Object[] getParams() {
+        return new Object[][] {
+            new Object[] {"0.99 0.7 0.7 0.7 0.7 0.7", 854328},
+            new Object[] {"0.999 0.3 0.3 0.95 0.9 0.9", 854328},
+            new Object[] {"0.99 0.7 0.7 0.7 0.7 0.7", 312547},
+            new Object[] {"0.999 0.3 0.3 0.95 0.9 0.9", 312547}
+        };
     }
 
-    @Parameterized.Parameter(0)
-    public String mDatasetAsString;
-
-    @Parameterized.Parameter(1)
-    public int mSeed;
-
     double[] mDataset;
     private Constructor<? extends XmlSerializer> mKxmlConstructor;
     private Constructor<? extends XmlSerializer> mFastConstructor;
@@ -100,8 +90,7 @@
     }
 
     @SuppressWarnings("unchecked")
-    @Before
-    public void setUp() throws Exception {
+    public void setUp(String datasetAsString) throws Exception {
         mKxmlConstructor =
                 (Constructor)
                         Class.forName("com.android.org.kxml2.io.KXmlSerializer").getConstructor();
@@ -109,28 +98,32 @@
                 (Constructor)
                         Class.forName("com.android.internal.util.FastXmlSerializer")
                                 .getConstructor();
-        String[] splitStrings = mDatasetAsString.split(" ");
+        String[] splitStrings = datasetAsString.split(" ");
         mDataset = new double[splitStrings.length];
         for (int i = 0; i < splitStrings.length; i++) {
             mDataset[i] = Double.parseDouble(splitStrings[i]);
         }
     }
 
-    private void internalTimeSerializer(Constructor<? extends XmlSerializer> ctor)
+    private void internalTimeSerializer(Constructor<? extends XmlSerializer> ctor, int seed)
             throws Exception {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            serializeRandomXml(ctor, mSeed);
+            serializeRandomXml(ctor, seed);
         }
     }
 
     @Test
-    public void timeKxml() throws Exception {
-        internalTimeSerializer(mKxmlConstructor);
+    @Parameters(method = "getParams")
+    public void timeKxml(String datasetAsString, int seed) throws Exception {
+        setUp(datasetAsString);
+        internalTimeSerializer(mKxmlConstructor, seed);
     }
 
     @Test
-    public void timeFast() throws Exception {
-        internalTimeSerializer(mFastConstructor);
+    @Parameters(method = "getParams")
+    public void timeFast(String datasetAsString, int seed) throws Exception {
+        setUp(datasetAsString);
+        internalTimeSerializer(mFastConstructor, seed);
     }
 }
diff --git a/apct-tests/perftests/core/src/android/libcore/ZipFilePerfTest.java b/apct-tests/perftests/core/src/android/libcore/ZipFilePerfTest.java
index 31c92ba..3a45d40 100644
--- a/apct-tests/perftests/core/src/android/libcore/ZipFilePerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/ZipFilePerfTest.java
@@ -20,12 +20,12 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
-import org.junit.Before;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -38,23 +38,18 @@
 import java.util.zip.ZipFile;
 import java.util.zip.ZipOutputStream;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class ZipFilePerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
     private File mFile;
 
-    @Parameters(name = "numEntries={0}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(new Object[][] {{128}, {1024}, {8192}});
     }
 
-    @Parameterized.Parameter(0)
-    public int numEntries;
-
-    @Before
-    public void setUp() throws Exception {
+    public void setUp(int numEntries) throws Exception {
         mFile = File.createTempFile(getClass().getName(), ".zip");
         mFile.deleteOnExit();
         writeEntries(new ZipOutputStream(new FileOutputStream(mFile)), numEntries, 0);
@@ -66,7 +61,9 @@
     }
 
     @Test
-    public void timeZipFileOpen() throws Exception {
+    @Parameters(method = "getData")
+    public void timeZipFileOpen(int numEntries) throws Exception {
+        setUp(numEntries);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             ZipFile zf = new ZipFile(mFile);
diff --git a/apct-tests/perftests/core/src/android/libcore/ZipFileReadPerfTest.java b/apct-tests/perftests/core/src/android/libcore/ZipFileReadPerfTest.java
index faa9628..2e89518 100644
--- a/apct-tests/perftests/core/src/android/libcore/ZipFileReadPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/ZipFileReadPerfTest.java
@@ -20,12 +20,13 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -39,21 +40,17 @@
 import java.util.zip.ZipFile;
 import java.util.zip.ZipOutputStream;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class ZipFileReadPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameters(name = "readBufferSize={0}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(new Object[][] {{1024}, {16384}, {65536}});
     }
 
     private File mFile;
 
-    @Parameterized.Parameter(0)
-    public int readBufferSize;
-
     @Before
     public void setUp() throws Exception {
         mFile = File.createTempFile(getClass().getName(), ".zip");
@@ -90,7 +87,8 @@
     }
 
     @Test
-    public void timeZipFileRead() throws Exception {
+    @Parameters(method = "getData")
+    public void timeZipFileRead(int readBufferSize) throws Exception {
         byte[] readBuffer = new byte[readBufferSize];
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/BitSetPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/BitSetPerfTest.java
index db5462c..2c0473e 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/BitSetPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/BitSetPerfTest.java
@@ -20,96 +20,99 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
-import org.junit.Before;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.util.Arrays;
 import java.util.BitSet;
 import java.util.Collection;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class BitSetPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameters(name = "mSize={0}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(new Object[][] {{1000}, {10000}});
     }
 
-    @Parameterized.Parameter(0)
-    public int mSize;
-
-    private BitSet mBitSet;
-
-    @Before
-    public void setUp() throws Exception {
-        mBitSet = new BitSet(mSize);
-    }
-
     @Test
-    public void timeIsEmptyTrue() {
+    @Parameters(method = "getData")
+    public void timeIsEmptyTrue(int size) {
+        BitSet bitSet = new BitSet(size);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            if (!mBitSet.isEmpty()) throw new RuntimeException();
+            if (!bitSet.isEmpty()) throw new RuntimeException();
         }
     }
 
     @Test
-    public void timeIsEmptyFalse() {
-        mBitSet.set(mBitSet.size() - 1);
+    @Parameters(method = "getData")
+    public void timeIsEmptyFalse(int size) {
+        BitSet bitSet = new BitSet(size);
+        bitSet.set(bitSet.size() - 1);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            if (mBitSet.isEmpty()) throw new RuntimeException();
+            if (bitSet.isEmpty()) throw new RuntimeException();
         }
     }
 
     @Test
-    public void timeGet() {
+    @Parameters(method = "getData")
+    public void timeGet(int size) {
+        BitSet bitSet = new BitSet(size);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         int i = 1;
         while (state.keepRunning()) {
-            mBitSet.get(++i % mSize);
+            bitSet.get(++i % size);
         }
     }
 
     @Test
-    public void timeClear() {
+    @Parameters(method = "getData")
+    public void timeClear(int size) {
+        BitSet bitSet = new BitSet(size);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         int i = 1;
         while (state.keepRunning()) {
-            mBitSet.clear(++i % mSize);
+            bitSet.clear(++i % size);
         }
     }
 
     @Test
-    public void timeSet() {
+    @Parameters(method = "getData")
+    public void timeSet(int size) {
+        BitSet bitSet = new BitSet(size);
         int i = 1;
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mBitSet.set(++i % mSize);
+            bitSet.set(++i % size);
         }
     }
 
     @Test
-    public void timeSetOn() {
+    @Parameters(method = "getData")
+    public void timeSetOn(int size) {
+        BitSet bitSet = new BitSet(size);
         int i = 1;
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mBitSet.set(++i % mSize, true);
+            bitSet.set(++i % size, true);
         }
     }
 
     @Test
-    public void timeSetOff() {
+    @Parameters(method = "getData")
+    public void timeSetOff(int size) {
+        BitSet bitSet = new BitSet(size);
         int i = 1;
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mBitSet.set(++i % mSize, false);
+            bitSet.set(++i % size, false);
         }
     }
 }
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/BreakIteratorPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/BreakIteratorPerfTest.java
index 3952c12..6a2ce58 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/BreakIteratorPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/BreakIteratorPerfTest.java
@@ -20,18 +20,19 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.text.BreakIterator;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Locale;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public final class BreakIteratorPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
@@ -41,36 +42,37 @@
                 Locale.US,
                 "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi mollis consequat"
                     + " nisl non pharetra. Praesent pretium vehicula odio sed ultrices. Aenean a"
-                    + " felis libero. Vivamus sed commodo nibh. Pellentesque turpis lectus, euismod"
-                    + " vel ante nec, cursus posuere orci. Suspendisse velit neque, fermentum"
-                    + " luctus ultrices in, ultrices vitae arcu. Duis tincidunt cursus lorem. Nam"
-                    + " ultricies accumsan quam vitae imperdiet. Pellentesque habitant morbi"
-                    + " tristique senectus et netus et malesuada fames ac turpis egestas. Quisque"
-                    + " aliquet pretium nisi, eget laoreet enim molestie sit amet. Class aptent"
-                    + " taciti sociosqu ad litora torquent per conubia nostra, per inceptos"
+                    + " felis libero. Vivamus sed commodo nibh. Pellentesque turpis lectus,"
+                    + " euismod vel ante nec, cursus posuere orci. Suspendisse velit neque,"
+                    + " fermentum luctus ultrices in, ultrices vitae arcu. Duis tincidunt cursus"
+                    + " lorem. Nam ultricies accumsan quam vitae imperdiet. Pellentesque habitant"
+                    + " morbi tristique senectus et netus et malesuada fames ac turpis egestas."
+                    + " Quisque aliquet pretium nisi, eget laoreet enim molestie sit amet. Class"
+                    + " aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos"
                     + " himenaeos.\n"
                     + "Nam dapibus aliquam lacus ac suscipit. Proin in nibh sit amet purus congue"
                     + " laoreet eget quis nisl. Morbi gravida dignissim justo, a venenatis ante"
-                    + " pulvinar at. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin"
-                    + " ultrices vestibulum dui, vel aliquam lacus aliquam quis. Duis fringilla"
-                    + " sapien ac lacus egestas, vel adipiscing elit euismod. Donec non tellus"
-                    + " odio. Donec gravida eu massa ac feugiat. Aliquam erat volutpat. Praesent id"
-                    + " adipiscing metus, nec laoreet enim. Aliquam vitae posuere turpis. Mauris ac"
-                    + " pharetra sem. In at placerat tortor. Vivamus ac vehicula neque. Cras"
-                    + " volutpat ullamcorper massa et varius. Praesent sagittis neque vitae nulla"
-                    + " euismod pharetra.\n"
+                    + " pulvinar at. Lorem ipsum dolor sit amet, consectetur adipiscing elit."
+                    + " Proin ultrices vestibulum dui, vel aliquam lacus aliquam quis. Duis"
+                    + " fringilla sapien ac lacus egestas, vel adipiscing elit euismod. Donec non"
+                    + " tellus odio. Donec gravida eu massa ac feugiat. Aliquam erat volutpat."
+                    + " Praesent id adipiscing metus, nec laoreet enim. Aliquam vitae posuere"
+                    + " turpis. Mauris ac pharetra sem. In at placerat tortor. Vivamus ac vehicula"
+                    + " neque. Cras volutpat ullamcorper massa et varius. Praesent sagittis neque"
+                    + " vitae nulla euismod pharetra.\n"
                     + "Sed placerat sapien non molestie sollicitudin. Nullam sit amet dictum quam."
                     + " Etiam tincidunt tortor vel pretium vehicula. Praesent fringilla ipsum vel"
                     + " velit luctus dignissim. Nulla massa ligula, mattis in enim et, mattis"
                     + " lacinia odio. Suspendisse tristique urna a orci commodo tempor. Duis"
                     + " lacinia egestas arcu a sollicitudin.\n"
                     + "In ac feugiat lacus. Nunc fermentum eu est at tristique. Pellentesque quis"
-                    + " ligula et orci placerat lacinia. Maecenas quis mauris diam. Etiam mi ipsum,"
-                    + " tempus in purus quis, euismod faucibus orci. Nulla facilisi. Praesent sit"
-                    + " amet sapien vel elit porta adipiscing. Phasellus sit amet volutpat diam.\n"
-                    + "Proin bibendum elit non lacus pharetra, quis eleifend tellus placerat. Nulla"
-                    + " facilisi. Maecenas ante diam, pellentesque mattis mattis in, porta ut"
-                    + " lorem. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices"
+                    + " ligula et orci placerat lacinia. Maecenas quis mauris diam. Etiam mi"
+                    + " ipsum, tempus in purus quis, euismod faucibus orci. Nulla facilisi."
+                    + " Praesent sit amet sapien vel elit porta adipiscing. Phasellus sit amet"
+                    + " volutpat diam.\n"
+                    + "Proin bibendum elit non lacus pharetra, quis eleifend tellus placerat."
+                    + " Nulla facilisi. Maecenas ante diam, pellentesque mattis mattis in, porta"
+                    + " ut lorem. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices"
                     + " posuere cubilia Curae; Nunc interdum tristique metus, in scelerisque odio"
                     + " fermentum eget. Cras nec venenatis lacus. Aenean euismod eget metus quis"
                     + " molestie. Cras tincidunt dolor ut massa ornare, in elementum lacus auctor."
@@ -80,29 +82,29 @@
         LONGPARA(
                 Locale.US,
                 "During dinner, Mr. Bennet scarcely spoke at all; but when the servants were"
-                    + " withdrawn, he thought it time to have some conversation with his guest, and"
-                    + " therefore started a subject in which he expected him to shine, by observing"
-                    + " that he seemed very fortunate in his patroness. Lady Catherine de Bourgh's"
-                    + " attention to his wishes, and consideration for his comfort, appeared very"
-                    + " remarkable. Mr. Bennet could not have chosen better. Mr. Collins was"
-                    + " eloquent in her praise. The subject elevated him to more than usual"
-                    + " solemnity of manner, and with a most important aspect he protested that"
-                    + " \"he had never in his life witnessed such behaviour in a person of"
-                    + " rank--such affability and condescension, as he had himself experienced from"
-                    + " Lady Catherine. She had been graciously pleased to approve of both of the"
-                    + " discourses which he had already had the honour of preaching before her. She"
-                    + " had also asked him twice to dine at Rosings, and had sent for him only the"
-                    + " Saturday before, to make up her pool of quadrille in the evening. Lady"
-                    + " Catherine was reckoned proud by many people he knew, but _he_ had never"
-                    + " seen anything but affability in her. She had always spoken to him as she"
-                    + " would to any other gentleman; she made not the smallest objection to his"
-                    + " joining in the society of the neighbourhood nor to his leaving the parish"
-                    + " occasionally for a week or two, to visit his relations. She had even"
-                    + " condescended to advise him to marry as soon as he could, provided he chose"
-                    + " with discretion; and had once paid him a visit in his humble parsonage,"
-                    + " where she had perfectly approved all the alterations he had been making,"
-                    + " and had even vouchsafed to suggest some herself--some shelves in the closet"
-                    + " up stairs.\""),
+                    + " withdrawn, he thought it time to have some conversation with his guest,"
+                    + " and therefore started a subject in which he expected him to shine, by"
+                    + " observing that he seemed very fortunate in his patroness. Lady Catherine"
+                    + " de Bourgh's attention to his wishes, and consideration for his comfort,"
+                    + " appeared very remarkable. Mr. Bennet could not have chosen better. Mr."
+                    + " Collins was eloquent in her praise. The subject elevated him to more than"
+                    + " usual solemnity of manner, and with a most important aspect he protested"
+                    + " that \"he had never in his life witnessed such behaviour in a person of"
+                    + " rank--such affability and condescension, as he had himself experienced"
+                    + " from Lady Catherine. She had been graciously pleased to approve of both of"
+                    + " the discourses which he had already had the honour of preaching before"
+                    + " her. She had also asked him twice to dine at Rosings, and had sent for him"
+                    + " only the Saturday before, to make up her pool of quadrille in the evening."
+                    + " Lady Catherine was reckoned proud by many people he knew, but _he_ had"
+                    + " never seen anything but affability in her. She had always spoken to him as"
+                    + " she would to any other gentleman; she made not the smallest objection to"
+                    + " his joining in the society of the neighbourhood nor to his leaving the"
+                    + " parish occasionally for a week or two, to visit his relations. She had"
+                    + " even condescended to advise him to marry as soon as he could, provided he"
+                    + " chose with discretion; and had once paid him a visit in his humble"
+                    + " parsonage, where she had perfectly approved all the alterations he had"
+                    + " been making, and had even vouchsafed to suggest some herself--some shelves"
+                    + " in the closet up stairs.\""),
         GERMAN(
                 Locale.GERMANY,
                 "Aber dieser Freiheit setzte endlich der Winter ein Ziel. Draußen auf den Feldern"
@@ -119,15 +121,14 @@
                     + " เดิมทีเป็นการผสมผสานกันระหว่างสำเนียงอยุธยาและชาวไทยเชื้อสายจีนรุ่นหลังที่"
                     + "พูดไทยแทนกลุ่มภาษาจีน"
                     + " ลักษณะเด่นคือมีการออกเสียงที่ชัดเจนและแข็งกระด้างซึ่งได้รับอิทธิพลจากภาษาแต"
-                    + "้จิ๋ว"
-                    + " การออกเสียงพยัญชนะ สระ การผันวรรณยุกต์ที่ในภาษาไทยมาตรฐาน"
+                    + "้จิ๋ว การออกเสียงพยัญชนะ สระ การผันวรรณยุกต์ที่ในภาษาไทยมาตรฐาน"
                     + " มาจากสำเนียงถิ่นนี้ในขณะที่ภาษาไทยสำเนียงอื่นล้วนเหน่อทั้งสิ้น"
                     + " คำศัพท์ที่ใช้ในสำเนียงกรุงเทพจำนวนมากได้รับมาจากกลุ่มภาษาจีนเช่นคำว่า โป๊,"
                     + " เฮ็ง, อาหมวย, อาซิ่ม ซึ่งมาจากภาษาแต้จิ๋ว และจากภาษาจีนเช่น ถู(涂), ชิ่ว(去"
                     + " อ่านว่า\"ชู่\") และคำว่า ทาย(猜 อ่านว่า \"ชาย\") เป็นต้น"
                     + " เนื่องจากสำเนียงกรุงเทพได้รับอิทธิพลมาจากภาษาจีนดังนั้นตัวอักษร \"ร\""
-                    + " มักออกเสียงเหมารวมเป็น \"ล\" หรือคำควบกล่ำบางคำถูกละทิ้งไปด้วยเช่น รู้ เป็น"
-                    + " ลู้, เรื่อง เป็น เลื่อง หรือ ประเทศ เป็น ปะเทศ"
+                    + " มักออกเสียงเหมารวมเป็น \"ล\" หรือคำควบกล่ำบางคำถูกละทิ้งไปด้วยเช่น รู้"
+                    + " เป็น ลู้, เรื่อง เป็น เลื่อง หรือ ประเทศ เป็น ปะเทศ"
                     + " เป็นต้นสร้างความลำบากให้แก่ต่างชาติที่ต้องการเรียนภาษาไทย"
                     + " แต่อย่างไรก็ตามผู้ที่พูดสำเนียงถิ่นนี้ก็สามารถออกอักขระภาษาไทยตามมาตรฐานได"
                     + "้อย่างถูกต้องเพียงแต่มักเผลอไม่ค่อยออกเสียง"),
@@ -151,8 +152,7 @@
         }
     }
 
-    @Parameters(name = "mText={0}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(
                 new Object[][] {
                     {Text.ACCENT}, {Text.BIDI}, {Text.EMOJI}, {Text.EMPTY}, {Text.GERMAN},
@@ -161,15 +161,13 @@
                 });
     }
 
-    @Parameterized.Parameter(0)
-    public Text mText;
-
     @Test
-    public void timeBreakIterator() {
+    @Parameters(method = "getData")
+    public void timeBreakIterator(Text text) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            BreakIterator it = BreakIterator.getLineInstance(mText.mLocale);
-            it.setText(mText.mText);
+            BreakIterator it = BreakIterator.getLineInstance(text.mLocale);
+            it.setText(text.mText);
 
             while (it.next() != BreakIterator.DONE) {
                 // Keep iterating
@@ -178,12 +176,13 @@
     }
 
     @Test
-    public void timeIcuBreakIterator() {
+    @Parameters(method = "getData")
+    public void timeIcuBreakIterator(Text text) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             android.icu.text.BreakIterator it =
-                    android.icu.text.BreakIterator.getLineInstance(mText.mLocale);
-            it.setText(mText.mText);
+                    android.icu.text.BreakIterator.getLineInstance(text.mLocale);
+            it.setText(text.mText);
 
             while (it.next() != android.icu.text.BreakIterator.DONE) {
                 // Keep iterating
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/BulkPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/BulkPerfTest.java
index 855bb9a..b7b7e83 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/BulkPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/BulkPerfTest.java
@@ -20,11 +20,12 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.io.File;
 import java.io.IOException;
@@ -34,13 +35,12 @@
 import java.util.Arrays;
 import java.util.Collection;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class BulkPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameters(name = "mAlign({0}), mSBuf({1}), mDBuf({2}), mSize({3})")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(
                 new Object[][] {
                     {true, MyBufferType.DIRECT, MyBufferType.DIRECT, 4096},
@@ -82,24 +82,12 @@
                 });
     }
 
-    @Parameterized.Parameter(0)
-    public boolean mAlign;
-
     enum MyBufferType {
         DIRECT,
         HEAP,
         MAPPED
     }
 
-    @Parameterized.Parameter(1)
-    public MyBufferType mSBuf;
-
-    @Parameterized.Parameter(2)
-    public MyBufferType mDBuf;
-
-    @Parameterized.Parameter(3)
-    public int mSize;
-
     public static ByteBuffer newBuffer(boolean aligned, MyBufferType bufferType, int bsize)
             throws IOException {
         int size = aligned ? bsize : bsize + 8 + 1;
@@ -126,13 +114,15 @@
     }
 
     @Test
-    public void timePut() throws Exception {
-        ByteBuffer src = BulkPerfTest.newBuffer(mAlign, mSBuf, mSize);
-        ByteBuffer data = BulkPerfTest.newBuffer(mAlign, mDBuf, mSize);
+    @Parameters(method = "getData")
+    public void timePut(boolean align, MyBufferType sBuf, MyBufferType dBuf, int size)
+            throws Exception {
+        ByteBuffer src = BulkPerfTest.newBuffer(align, sBuf, size);
+        ByteBuffer data = BulkPerfTest.newBuffer(align, dBuf, size);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAlign ? 0 : 1);
-            data.position(mAlign ? 0 : 1);
+            src.position(align ? 0 : 1);
+            data.position(align ? 0 : 1);
             src.put(data);
         }
     }
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferPerfTest.java
index 4bd7c4e..9ac36d0 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferPerfTest.java
@@ -20,11 +20,12 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.io.File;
 import java.io.IOException;
@@ -41,7 +42,7 @@
 import java.util.Arrays;
 import java.util.Collection;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class ByteBufferPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
@@ -49,15 +50,14 @@
     public enum MyByteOrder {
         BIG(ByteOrder.BIG_ENDIAN),
         LITTLE(ByteOrder.LITTLE_ENDIAN);
-        final ByteOrder mByteOrder;
+        final ByteOrder byteOrder;
 
-        MyByteOrder(ByteOrder mByteOrder) {
-            this.mByteOrder = mByteOrder;
+        MyByteOrder(ByteOrder byteOrder) {
+            this.byteOrder = byteOrder;
         }
     }
 
-    @Parameters(name = "mByteOrder={0}, mAligned={1}, mBufferType={2}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(
                 new Object[][] {
                     {MyByteOrder.BIG, true, MyBufferType.DIRECT},
@@ -75,21 +75,12 @@
                 });
     }
 
-    @Parameterized.Parameter(0)
-    public MyByteOrder mByteOrder;
-
-    @Parameterized.Parameter(1)
-    public boolean mAligned;
-
     enum MyBufferType {
         DIRECT,
         HEAP,
         MAPPED;
     }
 
-    @Parameterized.Parameter(2)
-    public MyBufferType mBufferType;
-
     public static ByteBuffer newBuffer(
             MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws IOException {
         int size = aligned ? 8192 : 8192 + 8 + 1;
@@ -115,7 +106,7 @@
                 result = fc.map(FileChannel.MapMode.READ_WRITE, 0, fc.size());
                 break;
         }
-        result.order(byteOrder.mByteOrder);
+        result.order(byteOrder.byteOrder);
         result.position(aligned ? 0 : 1);
         return result;
     }
@@ -125,11 +116,13 @@
     //
 
     @Test
-    public void timeByteBuffer_getByte() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_getByte(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAligned ? 0 : 1);
+            src.position(aligned ? 0 : 1);
             for (int i = 0; i < 1024; ++i) {
                 src.get();
             }
@@ -137,24 +130,28 @@
     }
 
     @Test
-    public void timeByteBuffer_getByteArray() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_getByteArray(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         byte[] dst = new byte[1024];
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             for (int i = 0; i < 1024; ++i) {
-                src.position(mAligned ? 0 : 1);
+                src.position(aligned ? 0 : 1);
                 src.get(dst);
             }
         }
     }
 
     @Test
-    public void timeByteBuffer_getByte_indexed() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_getByte_indexed(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAligned ? 0 : 1);
+            src.position(aligned ? 0 : 1);
             for (int i = 0; i < 1024; ++i) {
                 src.get(i);
             }
@@ -162,11 +159,13 @@
     }
 
     @Test
-    public void timeByteBuffer_getChar() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_getChar(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAligned ? 0 : 1);
+            src.position(aligned ? 0 : 1);
             for (int i = 0; i < 1024; ++i) {
                 src.getChar();
             }
@@ -174,9 +173,11 @@
     }
 
     @Test
-    public void timeCharBuffer_getCharArray() throws Exception {
+    @Parameters(method = "getData")
+    public void timeCharBuffer_getCharArray(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
         CharBuffer src =
-                ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asCharBuffer();
+                ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asCharBuffer();
         char[] dst = new char[1024];
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
@@ -188,11 +189,13 @@
     }
 
     @Test
-    public void timeByteBuffer_getChar_indexed() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_getChar_indexed(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAligned ? 0 : 1);
+            src.position(aligned ? 0 : 1);
             for (int i = 0; i < 1024; ++i) {
                 src.getChar(i * 2);
             }
@@ -200,11 +203,13 @@
     }
 
     @Test
-    public void timeByteBuffer_getDouble() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_getDouble(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAligned ? 0 : 1);
+            src.position(aligned ? 0 : 1);
             for (int i = 0; i < 1024; ++i) {
                 src.getDouble();
             }
@@ -212,9 +217,11 @@
     }
 
     @Test
-    public void timeDoubleBuffer_getDoubleArray() throws Exception {
+    @Parameters(method = "getData")
+    public void timeDoubleBuffer_getDoubleArray(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
         DoubleBuffer src =
-                ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asDoubleBuffer();
+                ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asDoubleBuffer();
         double[] dst = new double[1024];
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
@@ -226,11 +233,13 @@
     }
 
     @Test
-    public void timeByteBuffer_getFloat() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_getFloat(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAligned ? 0 : 1);
+            src.position(aligned ? 0 : 1);
             for (int i = 0; i < 1024; ++i) {
                 src.getFloat();
             }
@@ -238,9 +247,11 @@
     }
 
     @Test
-    public void timeFloatBuffer_getFloatArray() throws Exception {
+    @Parameters(method = "getData")
+    public void timeFloatBuffer_getFloatArray(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
         FloatBuffer src =
-                ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asFloatBuffer();
+                ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asFloatBuffer();
         float[] dst = new float[1024];
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
@@ -252,11 +263,13 @@
     }
 
     @Test
-    public void timeByteBuffer_getInt() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_getInt(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAligned ? 0 : 1);
+            src.position(aligned ? 0 : 1);
             for (int i = 0; i < 1024; ++i) {
                 src.getInt();
             }
@@ -264,9 +277,10 @@
     }
 
     @Test
-    public void timeIntBuffer_getIntArray() throws Exception {
-        IntBuffer src =
-                ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asIntBuffer();
+    @Parameters(method = "getData")
+    public void timeIntBuffer_getIntArray(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        IntBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asIntBuffer();
         int[] dst = new int[1024];
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
@@ -278,11 +292,13 @@
     }
 
     @Test
-    public void timeByteBuffer_getLong() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_getLong(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAligned ? 0 : 1);
+            src.position(aligned ? 0 : 1);
             for (int i = 0; i < 1024; ++i) {
                 src.getLong();
             }
@@ -290,9 +306,11 @@
     }
 
     @Test
-    public void timeLongBuffer_getLongArray() throws Exception {
+    @Parameters(method = "getData")
+    public void timeLongBuffer_getLongArray(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
         LongBuffer src =
-                ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asLongBuffer();
+                ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asLongBuffer();
         long[] dst = new long[1024];
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
@@ -304,11 +322,13 @@
     }
 
     @Test
-    public void timeByteBuffer_getShort() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_getShort(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAligned ? 0 : 1);
+            src.position(aligned ? 0 : 1);
             for (int i = 0; i < 1024; ++i) {
                 src.getShort();
             }
@@ -316,9 +336,11 @@
     }
 
     @Test
-    public void timeShortBuffer_getShortArray() throws Exception {
+    @Parameters(method = "getData")
+    public void timeShortBuffer_getShortArray(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
         ShortBuffer src =
-                ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asShortBuffer();
+                ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asShortBuffer();
         short[] dst = new short[1024];
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
@@ -334,8 +356,10 @@
     //
 
     @Test
-    public void timeByteBuffer_putByte() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_putByte(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             src.position(0);
@@ -346,24 +370,28 @@
     }
 
     @Test
-    public void timeByteBuffer_putByteArray() throws Exception {
-        ByteBuffer dst = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_putByteArray(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer dst = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         byte[] src = new byte[1024];
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             for (int i = 0; i < 1024; ++i) {
-                dst.position(mAligned ? 0 : 1);
+                dst.position(aligned ? 0 : 1);
                 dst.put(src);
             }
         }
     }
 
     @Test
-    public void timeByteBuffer_putChar() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_putChar(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAligned ? 0 : 1);
+            src.position(aligned ? 0 : 1);
             for (int i = 0; i < 1024; ++i) {
                 src.putChar(' ');
             }
@@ -371,9 +399,11 @@
     }
 
     @Test
-    public void timeCharBuffer_putCharArray() throws Exception {
+    @Parameters(method = "getData")
+    public void timeCharBuffer_putCharArray(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
         CharBuffer dst =
-                ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asCharBuffer();
+                ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asCharBuffer();
         char[] src = new char[1024];
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
@@ -385,11 +415,13 @@
     }
 
     @Test
-    public void timeByteBuffer_putDouble() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_putDouble(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAligned ? 0 : 1);
+            src.position(aligned ? 0 : 1);
             for (int i = 0; i < 1024; ++i) {
                 src.putDouble(0.0);
             }
@@ -397,9 +429,11 @@
     }
 
     @Test
-    public void timeDoubleBuffer_putDoubleArray() throws Exception {
+    @Parameters(method = "getData")
+    public void timeDoubleBuffer_putDoubleArray(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
         DoubleBuffer dst =
-                ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asDoubleBuffer();
+                ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asDoubleBuffer();
         double[] src = new double[1024];
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
@@ -411,11 +445,13 @@
     }
 
     @Test
-    public void timeByteBuffer_putFloat() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_putFloat(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAligned ? 0 : 1);
+            src.position(aligned ? 0 : 1);
             for (int i = 0; i < 1024; ++i) {
                 src.putFloat(0.0f);
             }
@@ -423,9 +459,11 @@
     }
 
     @Test
-    public void timeFloatBuffer_putFloatArray() throws Exception {
+    @Parameters(method = "getData")
+    public void timeFloatBuffer_putFloatArray(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
         FloatBuffer dst =
-                ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asFloatBuffer();
+                ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asFloatBuffer();
         float[] src = new float[1024];
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
@@ -437,11 +475,13 @@
     }
 
     @Test
-    public void timeByteBuffer_putInt() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_putInt(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAligned ? 0 : 1);
+            src.position(aligned ? 0 : 1);
             for (int i = 0; i < 1024; ++i) {
                 src.putInt(0);
             }
@@ -449,9 +489,10 @@
     }
 
     @Test
-    public void timeIntBuffer_putIntArray() throws Exception {
-        IntBuffer dst =
-                ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asIntBuffer();
+    @Parameters(method = "getData")
+    public void timeIntBuffer_putIntArray(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        IntBuffer dst = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asIntBuffer();
         int[] src = new int[1024];
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
@@ -463,11 +504,13 @@
     }
 
     @Test
-    public void timeByteBuffer_putLong() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_putLong(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAligned ? 0 : 1);
+            src.position(aligned ? 0 : 1);
             for (int i = 0; i < 1024; ++i) {
                 src.putLong(0L);
             }
@@ -475,9 +518,11 @@
     }
 
     @Test
-    public void timeLongBuffer_putLongArray() throws Exception {
+    @Parameters(method = "getData")
+    public void timeLongBuffer_putLongArray(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
         LongBuffer dst =
-                ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asLongBuffer();
+                ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asLongBuffer();
         long[] src = new long[1024];
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
@@ -489,11 +534,13 @@
     }
 
     @Test
-    public void timeByteBuffer_putShort() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeByteBuffer_putShort(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAligned ? 0 : 1);
+            src.position(aligned ? 0 : 1);
             for (int i = 0; i < 1024; ++i) {
                 src.putShort((short) 0);
             }
@@ -501,9 +548,11 @@
     }
 
     @Test
-    public void timeShortBuffer_putShortArray() throws Exception {
+    @Parameters(method = "getData")
+    public void timeShortBuffer_putShortArray(
+            MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception {
         ShortBuffer dst =
-                ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asShortBuffer();
+                ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asShortBuffer();
         short[] src = new short[1024];
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
@@ -515,6 +564,7 @@
     }
 
     @Test
+    @Parameters(method = "getData")
     public void time_new_byteArray() throws Exception {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
@@ -523,6 +573,7 @@
     }
 
     @Test
+    @Parameters(method = "getData")
     public void time_ByteBuffer_allocate() throws Exception {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferScalarVersusVectorPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferScalarVersusVectorPerfTest.java
index 81f9e59..5dd9d6e 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferScalarVersusVectorPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferScalarVersusVectorPerfTest.java
@@ -20,23 +20,23 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.nio.ByteBuffer;
 import java.util.Arrays;
 import java.util.Collection;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class ByteBufferScalarVersusVectorPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameters(name = "mByteOrder={0}, mAligned={1}, mBufferType={2}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(
                 new Object[][] {
                     {
@@ -102,19 +102,15 @@
                 });
     }
 
-    @Parameterized.Parameter(0)
-    public ByteBufferPerfTest.MyByteOrder mByteOrder;
-
-    @Parameterized.Parameter(1)
-    public boolean mAligned;
-
-    @Parameterized.Parameter(2)
-    public ByteBufferPerfTest.MyBufferType mBufferType;
-
     @Test
-    public void timeManualByteBufferCopy() throws Exception {
-        ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
-        ByteBuffer dst = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType);
+    @Parameters(method = "getData")
+    public void timeManualByteBufferCopy(
+            ByteBufferPerfTest.MyByteOrder byteOrder,
+            boolean aligned,
+            ByteBufferPerfTest.MyBufferType bufferType)
+            throws Exception {
+        ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
+        ByteBuffer dst = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             src.position(0);
@@ -126,23 +122,25 @@
     }
 
     @Test
-    public void timeByteBufferBulkGet() throws Exception {
-        ByteBuffer src = ByteBuffer.allocate(mAligned ? 8192 : 8192 + 1);
+    @Parameters({"true", "false"})
+    public void timeByteBufferBulkGet(boolean aligned) throws Exception {
+        ByteBuffer src = ByteBuffer.allocate(aligned ? 8192 : 8192 + 1);
         byte[] dst = new byte[8192];
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAligned ? 0 : 1);
+            src.position(aligned ? 0 : 1);
             src.get(dst, 0, dst.length);
         }
     }
 
     @Test
-    public void timeDirectByteBufferBulkGet() throws Exception {
-        ByteBuffer src = ByteBuffer.allocateDirect(mAligned ? 8192 : 8192 + 1);
+    @Parameters({"true", "false"})
+    public void timeDirectByteBufferBulkGet(boolean aligned) throws Exception {
+        ByteBuffer src = ByteBuffer.allocateDirect(aligned ? 8192 : 8192 + 1);
         byte[] dst = new byte[8192];
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            src.position(mAligned ? 0 : 1);
+            src.position(aligned ? 0 : 1);
             src.get(dst, 0, dst.length);
         }
     }
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/CharacterPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/CharacterPerfTest.java
index 28ec6de..0a59899 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/CharacterPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/CharacterPerfTest.java
@@ -20,12 +20,12 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
-import org.junit.Before;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -34,13 +34,12 @@
  * Tests various Character methods, intended for testing multiple implementations against each
  * other.
  */
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class CharacterPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameters(name = "mCharacterSet({0}), mOverload({1})")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(
                 new Object[][] {
                     {CharacterSet.ASCII, Overload.CHAR},
@@ -50,17 +49,10 @@
                 });
     }
 
-    @Parameterized.Parameter(0)
-    public CharacterSet mCharacterSet;
-
-    @Parameterized.Parameter(1)
-    public Overload mOverload;
-
     private char[] mChars;
 
-    @Before
-    public void setUp() throws Exception {
-        this.mChars = mCharacterSet.mChars;
+    public void setUp(CharacterSet characterSet) {
+        this.mChars = characterSet.mChars;
     }
 
     public enum Overload {
@@ -87,10 +79,12 @@
 
     // A fake benchmark to give us a baseline.
     @Test
-    public void timeIsSpace() {
+    @Parameters(method = "getData")
+    public void timeIsSpace(CharacterSet characterSet, Overload overload) {
+        setUp(characterSet);
         boolean fake = false;
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        if (mOverload == Overload.CHAR) {
+        if (overload == Overload.CHAR) {
             while (state.keepRunning()) {
                 for (int ch = 0; ch < 65536; ++ch) {
                     fake ^= ((char) ch == ' ');
@@ -106,9 +100,11 @@
     }
 
     @Test
-    public void timeDigit() {
+    @Parameters(method = "getData")
+    public void timeDigit(CharacterSet characterSet, Overload overload) {
+        setUp(characterSet);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        if (mOverload == Overload.CHAR) {
+        if (overload == Overload.CHAR) {
             while (state.keepRunning()) {
                 for (int ch = 0; ch < 65536; ++ch) {
                     Character.digit(mChars[ch], 10);
@@ -124,9 +120,11 @@
     }
 
     @Test
-    public void timeGetNumericValue() {
+    @Parameters(method = "getData")
+    public void timeGetNumericValue(CharacterSet characterSet, Overload overload) {
+        setUp(characterSet);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        if (mOverload == Overload.CHAR) {
+        if (overload == Overload.CHAR) {
             while (state.keepRunning()) {
                 for (int ch = 0; ch < 65536; ++ch) {
                     Character.getNumericValue(mChars[ch]);
@@ -142,9 +140,11 @@
     }
 
     @Test
-    public void timeIsDigit() {
+    @Parameters(method = "getData")
+    public void timeIsDigit(CharacterSet characterSet, Overload overload) {
+        setUp(characterSet);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        if (mOverload == Overload.CHAR) {
+        if (overload == Overload.CHAR) {
             while (state.keepRunning()) {
                 for (int ch = 0; ch < 65536; ++ch) {
                     Character.isDigit(mChars[ch]);
@@ -160,9 +160,11 @@
     }
 
     @Test
-    public void timeIsIdentifierIgnorable() {
+    @Parameters(method = "getData")
+    public void timeIsIdentifierIgnorable(CharacterSet characterSet, Overload overload) {
+        setUp(characterSet);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        if (mOverload == Overload.CHAR) {
+        if (overload == Overload.CHAR) {
             while (state.keepRunning()) {
                 for (int ch = 0; ch < 65536; ++ch) {
                     Character.isIdentifierIgnorable(mChars[ch]);
@@ -178,9 +180,11 @@
     }
 
     @Test
-    public void timeIsJavaIdentifierPart() {
+    @Parameters(method = "getData")
+    public void timeIsJavaIdentifierPart(CharacterSet characterSet, Overload overload) {
+        setUp(characterSet);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        if (mOverload == Overload.CHAR) {
+        if (overload == Overload.CHAR) {
             while (state.keepRunning()) {
                 for (int ch = 0; ch < 65536; ++ch) {
                     Character.isJavaIdentifierPart(mChars[ch]);
@@ -196,9 +200,11 @@
     }
 
     @Test
-    public void timeIsJavaIdentifierStart() {
+    @Parameters(method = "getData")
+    public void timeIsJavaIdentifierStart(CharacterSet characterSet, Overload overload) {
+        setUp(characterSet);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        if (mOverload == Overload.CHAR) {
+        if (overload == Overload.CHAR) {
             while (state.keepRunning()) {
                 for (int ch = 0; ch < 65536; ++ch) {
                     Character.isJavaIdentifierStart(mChars[ch]);
@@ -214,9 +220,11 @@
     }
 
     @Test
-    public void timeIsLetter() {
+    @Parameters(method = "getData")
+    public void timeIsLetter(CharacterSet characterSet, Overload overload) {
+        setUp(characterSet);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        if (mOverload == Overload.CHAR) {
+        if (overload == Overload.CHAR) {
             while (state.keepRunning()) {
                 for (int ch = 0; ch < 65536; ++ch) {
                     Character.isLetter(mChars[ch]);
@@ -232,9 +240,11 @@
     }
 
     @Test
-    public void timeIsLetterOrDigit() {
+    @Parameters(method = "getData")
+    public void timeIsLetterOrDigit(CharacterSet characterSet, Overload overload) {
+        setUp(characterSet);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        if (mOverload == Overload.CHAR) {
+        if (overload == Overload.CHAR) {
             while (state.keepRunning()) {
                 for (int ch = 0; ch < 65536; ++ch) {
                     Character.isLetterOrDigit(mChars[ch]);
@@ -250,9 +260,11 @@
     }
 
     @Test
-    public void timeIsLowerCase() {
+    @Parameters(method = "getData")
+    public void timeIsLowerCase(CharacterSet characterSet, Overload overload) {
+        setUp(characterSet);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        if (mOverload == Overload.CHAR) {
+        if (overload == Overload.CHAR) {
             while (state.keepRunning()) {
                 for (int ch = 0; ch < 65536; ++ch) {
                     Character.isLowerCase(mChars[ch]);
@@ -268,9 +280,11 @@
     }
 
     @Test
-    public void timeIsSpaceChar() {
+    @Parameters(method = "getData")
+    public void timeIsSpaceChar(CharacterSet characterSet, Overload overload) {
+        setUp(characterSet);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        if (mOverload == Overload.CHAR) {
+        if (overload == Overload.CHAR) {
             while (state.keepRunning()) {
                 for (int ch = 0; ch < 65536; ++ch) {
                     Character.isSpaceChar(mChars[ch]);
@@ -286,9 +300,11 @@
     }
 
     @Test
-    public void timeIsUpperCase() {
+    @Parameters(method = "getData")
+    public void timeIsUpperCase(CharacterSet characterSet, Overload overload) {
+        setUp(characterSet);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        if (mOverload == Overload.CHAR) {
+        if (overload == Overload.CHAR) {
             while (state.keepRunning()) {
                 for (int ch = 0; ch < 65536; ++ch) {
                     Character.isUpperCase(mChars[ch]);
@@ -304,9 +320,11 @@
     }
 
     @Test
-    public void timeIsWhitespace() {
+    @Parameters(method = "getData")
+    public void timeIsWhitespace(CharacterSet characterSet, Overload overload) {
+        setUp(characterSet);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        if (mOverload == Overload.CHAR) {
+        if (overload == Overload.CHAR) {
             while (state.keepRunning()) {
                 for (int ch = 0; ch < 65536; ++ch) {
                     Character.isWhitespace(mChars[ch]);
@@ -322,9 +340,11 @@
     }
 
     @Test
-    public void timeToLowerCase() {
+    @Parameters(method = "getData")
+    public void timeToLowerCase(CharacterSet characterSet, Overload overload) {
+        setUp(characterSet);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        if (mOverload == Overload.CHAR) {
+        if (overload == Overload.CHAR) {
             while (state.keepRunning()) {
                 for (int ch = 0; ch < 65536; ++ch) {
                     Character.toLowerCase(mChars[ch]);
@@ -340,9 +360,11 @@
     }
 
     @Test
-    public void timeToUpperCase() {
+    @Parameters(method = "getData")
+    public void timeToUpperCase(CharacterSet characterSet, Overload overload) {
+        setUp(characterSet);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        if (mOverload == Overload.CHAR) {
+        if (overload == Overload.CHAR) {
             while (state.keepRunning()) {
                 for (int ch = 0; ch < 65536; ++ch) {
                     Character.toUpperCase(mChars[ch]);
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/CharsetForNamePerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/CharsetForNamePerfTest.java
index 603b182..8da13a9 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/CharsetForNamePerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/CharsetForNamePerfTest.java
@@ -20,44 +20,40 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
 
 import java.nio.charset.Charset;
-import java.util.Arrays;
-import java.util.Collection;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class CharsetForNamePerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameterized.Parameters(name = "mCharsetName({0})")
-    public static Collection<Object[]> data() {
-        return Arrays.asList(
-                new Object[][] {
-                    {"UTF-16"},
-                    {"UTF-8"},
-                    {"UTF8"},
-                    {"ISO-8859-1"},
-                    {"8859_1"},
-                    {"ISO-8859-2"},
-                    {"8859_2"},
-                    {"US-ASCII"},
-                    {"ASCII"},
-                });
+    public static String[] charsetNames() {
+        return new String[] {
+            "UTF-16",
+            "UTF-8",
+            "UTF8",
+            "ISO-8859-1",
+            "8859_1",
+            "ISO-8859-2",
+            "8859_2",
+            "US-ASCII",
+            "ASCII",
+        };
     }
 
-    @Parameterized.Parameter(0)
-    public String mCharsetName;
-
     @Test
-    public void timeCharsetForName() throws Exception {
+    @Parameters(method = "charsetNames")
+    public void timeCharsetForName(String charsetName) throws Exception {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            Charset.forName(mCharsetName);
+            Charset.forName(charsetName);
         }
     }
 }
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/CharsetPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/CharsetPerfTest.java
index 437d186..048c50f 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/CharsetPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/CharsetPerfTest.java
@@ -20,22 +20,22 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.util.Arrays;
 import java.util.Collection;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class CharsetPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameters(name = "mLength({0}), mName({1})")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(
                 new Object[][] {
                     {1, "UTF-16"},
@@ -86,24 +86,20 @@
                 });
     }
 
-    @Parameterized.Parameter(0)
-    public int mLength;
-
-    @Parameterized.Parameter(1)
-    public String mName;
-
     @Test
-    public void time_new_String_BString() throws Exception {
-        byte[] bytes = makeBytes(makeString(mLength));
+    @Parameters(method = "getData")
+    public void time_new_String_BString(int length, String name) throws Exception {
+        byte[] bytes = makeBytes(makeString(length));
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            new String(bytes, mName);
+            new String(bytes, name);
         }
     }
 
     @Test
-    public void time_new_String_BII() throws Exception {
-        byte[] bytes = makeBytes(makeString(mLength));
+    @Parameters(method = "getData")
+    public void time_new_String_BII(int length, String name) throws Exception {
+        byte[] bytes = makeBytes(makeString(length));
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             new String(bytes, 0, bytes.length);
@@ -111,20 +107,22 @@
     }
 
     @Test
-    public void time_new_String_BIIString() throws Exception {
-        byte[] bytes = makeBytes(makeString(mLength));
+    @Parameters(method = "getData")
+    public void time_new_String_BIIString(int length, String name) throws Exception {
+        byte[] bytes = makeBytes(makeString(length));
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            new String(bytes, 0, bytes.length, mName);
+            new String(bytes, 0, bytes.length, name);
         }
     }
 
     @Test
-    public void time_String_getBytes() throws Exception {
-        String string = makeString(mLength);
+    @Parameters(method = "getData")
+    public void time_String_getBytes(int length, String name) throws Exception {
+        String string = makeString(length);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            string.getBytes(mName);
+            string.getBytes(name);
         }
     }
 
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java
index 15c27f2..42b0588 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java
@@ -20,11 +20,12 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
-import org.junit.Before;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
 
 import java.security.spec.AlgorithmParameterSpec;
 import java.util.ArrayList;
@@ -42,17 +43,13 @@
  * Cipher benchmarks. Only runs on AES currently because of the combinatorial explosion of the test
  * as it stands.
  */
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class CipherPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameterized.Parameters(
-            name =
-                    "mMode({0}), mPadding({1}), mKeySize({2}), mInputSize({3}),"
-                            + " mImplementation({4})")
-    public static Collection cases() {
-        int[] mKeySizes = new int[] {128, 192, 256};
+    public static Collection getCases() {
+        int[] keySizes = new int[] {128, 192, 256};
         int[] inputSizes = new int[] {16, 32, 64, 128, 1024, 8192};
         final List<Object[]> params = new ArrayList<>();
         for (Mode mode : Mode.values()) {
@@ -71,11 +68,11 @@
                             && implementation == Implementation.OpenSSL) {
                         continue;
                     }
-                    for (int mKeySize : mKeySizes) {
+                    for (int keySize : keySizes) {
                         for (int inputSize : inputSizes) {
                             params.add(
                                     new Object[] {
-                                        mode, padding, mKeySize, inputSize, implementation
+                                        mode, padding, keySize, inputSize, implementation
                                     });
                         }
                     }
@@ -107,9 +104,6 @@
         AES,
     };
 
-    @Parameterized.Parameter(0)
-    public Mode mMode;
-
     public enum Mode {
         CBC,
         CFB,
@@ -118,23 +112,11 @@
         OFB,
     };
 
-    @Parameterized.Parameter(1)
-    public Padding mPadding;
-
     public enum Padding {
         NOPADDING,
         PKCS1PADDING,
     };
 
-    @Parameterized.Parameter(2)
-    public int mKeySize;
-
-    @Parameterized.Parameter(3)
-    public int mInputSize;
-
-    @Parameterized.Parameter(4)
-    public Implementation mImplementation;
-
     public enum Implementation {
         OpenSSL,
         BouncyCastle
@@ -156,21 +138,20 @@
 
     private AlgorithmParameterSpec mSpec;
 
-    @Before
-    public void setUp() throws Exception {
-        mCipherAlgorithm =
-                mAlgorithm.toString() + "/" + mMode.toString() + "/" + mPadding.toString();
+    public void setUp(Mode mode, Padding padding, int keySize, Implementation implementation)
+            throws Exception {
+        mCipherAlgorithm = mAlgorithm.toString() + "/" + mode.toString() + "/" + padding.toString();
 
         String mKeyAlgorithm = mAlgorithm.toString();
-        mKey = sKeySizes.get(mKeySize);
+        mKey = sKeySizes.get(keySize);
         if (mKey == null) {
             KeyGenerator generator = KeyGenerator.getInstance(mKeyAlgorithm);
-            generator.init(mKeySize);
+            generator.init(keySize);
             mKey = generator.generateKey();
-            sKeySizes.put(mKeySize, mKey);
+            sKeySizes.put(keySize, mKey);
         }
 
-        switch (mImplementation) {
+        switch (implementation) {
             case OpenSSL:
                 mProviderName = "AndroidOpenSSL";
                 break;
@@ -178,10 +159,10 @@
                 mProviderName = "BC";
                 break;
             default:
-                throw new RuntimeException(mImplementation.toString());
+                throw new RuntimeException(implementation.toString());
         }
 
-        if (mMode != Mode.ECB) {
+        if (mode != Mode.ECB) {
             mSpec = new IvParameterSpec(IV);
         }
 
@@ -193,18 +174,26 @@
     }
 
     @Test
-    public void timeEncrypt() throws Exception {
+    @Parameters(method = "getCases")
+    public void timeEncrypt(
+            Mode mode, Padding padding, int keySize, int inputSize, Implementation implementation)
+            throws Exception {
+        setUp(mode, padding, keySize, implementation);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mCipherEncrypt.doFinal(DATA, 0, mInputSize, mOutput);
+            mCipherEncrypt.doFinal(DATA, 0, inputSize, mOutput);
         }
     }
 
     @Test
-    public void timeDecrypt() throws Exception {
+    @Parameters(method = "getCases")
+    public void timeDecrypt(
+            Mode mode, Padding padding, int keySize, int inputSize, Implementation implementation)
+            throws Exception {
+        setUp(mode, padding, keySize, implementation);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mCipherDecrypt.doFinal(DATA, 0, mInputSize, mOutput);
+            mCipherDecrypt.doFinal(DATA, 0, inputSize, mOutput);
         }
     }
 }
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/CollectionsPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/CollectionsPerfTest.java
index a89efff..69197c3 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/CollectionsPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/CollectionsPerfTest.java
@@ -20,11 +20,12 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -35,19 +36,15 @@
 import java.util.Random;
 import java.util.Vector;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class CollectionsPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameters(name = "mArrayListLength({0})")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(new Object[][] {{4}, {16}, {64}, {256}, {1024}});
     }
 
-    @Parameterized.Parameter(0)
-    public int arrayListLength;
-
     public static Comparator<Integer> REVERSE =
             new Comparator<Integer>() {
                 @Override
@@ -59,7 +56,8 @@
             };
 
     @Test
-    public void timeSort_arrayList() throws Exception {
+    @Parameters(method = "getData")
+    public void timeSort_arrayList(int arrayListLength) throws Exception {
         List<Integer> input = buildList(arrayListLength, ArrayList.class);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
@@ -68,7 +66,8 @@
     }
 
     @Test
-    public void timeSortWithComparator_arrayList() throws Exception {
+    @Parameters(method = "getData")
+    public void timeSortWithComparator_arrayList(int arrayListLength) throws Exception {
         List<Integer> input = buildList(arrayListLength, ArrayList.class);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
@@ -77,7 +76,8 @@
     }
 
     @Test
-    public void timeSort_vector() throws Exception {
+    @Parameters(method = "getData")
+    public void timeSort_vector(int arrayListLength) throws Exception {
         List<Integer> input = buildList(arrayListLength, Vector.class);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
@@ -86,7 +86,8 @@
     }
 
     @Test
-    public void timeSortWithComparator_vector() throws Exception {
+    @Parameters(method = "getData")
+    public void timeSortWithComparator_vector(int arrayListLength) throws Exception {
         List<Integer> input = buildList(arrayListLength, Vector.class);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/EqualsHashCodePerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/EqualsHashCodePerfTest.java
index 4ff3ba5..8391203 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/EqualsHashCodePerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/EqualsHashCodePerfTest.java
@@ -20,11 +20,12 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
-import org.junit.Before;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
 
 import java.net.URI;
 import java.net.URL;
@@ -32,39 +33,39 @@
 import java.util.Collection;
 import java.util.List;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public final class EqualsHashCodePerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
     private enum Type {
         URI() {
-            @Override Object newInstance(String text) throws Exception {
+            @Override
+            Object newInstance(String text) throws Exception {
                 return new URI(text);
             }
         },
         URL() {
-            @Override Object newInstance(String text) throws Exception {
+            @Override
+            Object newInstance(String text) throws Exception {
                 return new URL(text);
             }
         };
+
         abstract Object newInstance(String text) throws Exception;
     }
 
-    private static final String QUERY = "%E0%AE%A8%E0%AE%BE%E0%AE%AE%E0%AF%8D+%E0%AE%AE%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AE%BF%E0%AE%AF%E0%AE%AE%E0%AE%BE%E0%AE%A9%2C+%E0%AE%9A%E0%AF%81%E0%AE%B5%E0%AE%BE%E0%AE%B0%E0%AE%B8%E0%AF%8D%E0%AE%AF%E0%AE%AE%E0%AE%BE%E0%AE%A9+%E0%AE%87%E0%AE%B0%E0%AF%81%E0%AE%AA%E0%AF%8D%E0%AE%AA%E0%AF%87%E0%AE%BE%E0%AE%AE%E0%AF%8D%2C+%E0%AE%86%E0%AE%A9%E0%AE%BE%E0%AE%B2%E0%AF%8D+%E0%AE%9A%E0%AE%BF%E0%AE%B2+%E0%AE%A8%E0%AF%87%E0%AE%B0%E0%AE%99%E0%AF%8D%E0%AE%95%E0%AE%B3%E0%AE%BF%E0%AE%B2%E0%AF%8D+%E0%AE%9A%E0%AF%82%E0%AE%B4%E0%AF%8D%E0%AE%A8%E0%AE%BF%E0%AE%B2%E0%AF%88+%E0%AE%8F%E0%AE%B1%E0%AF%8D%E0%AE%AA%E0%AE%9F%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%8E%E0%AE%A9%E0%AF%8D%E0%AE%AA%E0%AE%A4%E0%AE%BE%E0%AE%B2%E0%AF%8D+%E0%AE%AA%E0%AE%A3%E0%AE%BF%E0%AE%AF%E0%AF%88%E0%AE%AF%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%B5%E0%AE%B2%E0%AE%BF+%E0%AE%85%E0%AE%B5%E0%AE%B0%E0%AF%88+%E0%AE%9A%E0%AE%BF%E0%AE%B2+%E0%AE%AA%E0%AF%86%E0%AE%B0%E0%AE%BF%E0%AE%AF+%E0%AE%95%E0%AF%86%E0%AE%BE%E0%AE%B3%E0%AF%8D%E0%AE%AE%E0%AF%81%E0%AE%A4%E0%AE%B2%E0%AF%8D+%E0%AE%AE%E0%AF%81%E0%AE%9F%E0%AE%BF%E0%AE%AF%E0%AF%81%E0%AE%AE%E0%AF%8D.+%E0%AE%85%E0%AE%A4%E0%AF%81+%E0%AE%9A%E0%AE%BF%E0%AE%B2+%E0%AE%A8%E0%AE%A9%E0%AF%8D%E0%AE%AE%E0%AF%88%E0%AE%95%E0%AE%B3%E0%AF%88+%E0%AE%AA%E0%AF%86%E0%AE%B1+%E0%AE%A4%E0%AE%B5%E0%AE%BF%E0%AE%B0%2C+%E0%AE%8E%E0%AE%AA%E0%AF%8D%E0%AE%AA%E0%AF%87%E0%AE%BE%E0%AE%A4%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%89%E0%AE%B4%E0%AF%88%E0%AE%95%E0%AF%8D%E0%AE%95+%E0%AE%89%E0%AE%9F%E0%AE%B1%E0%AF%8D%E0%AE%AA%E0%AE%AF%E0%AE%BF%E0%AE%B1%E0%AF%8D%E0%AE%9A%E0%AE%BF+%E0%AE%AE%E0%AF%87%E0%AE%B1%E0%AF%8D%E0%AE%95%E0%AF%86%E0%AE%BE%E0%AE%B3%E0%AF%8D%E0%AE%95%E0%AE%BF%E0%AE%B1%E0%AE%A4%E0%AF%81+%E0%AE%8E%E0%AE%99%E0%AF%8D%E0%AE%95%E0%AE%B3%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AF%81+%E0%AE%87%E0%AE%A4%E0%AF%81+%E0%AE%92%E0%AE%B0%E0%AF%81+%E0%AE%9A%E0%AE%BF%E0%AE%B1%E0%AE%BF%E0%AE%AF+%E0%AE%89%E0%AE%A4%E0%AE%BE%E0%AE%B0%E0%AE%A3%E0%AE%AE%E0%AF%8D%2C+%E0%AE%8E%E0%AE%9F%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95.+%E0%AE%B0%E0%AE%AF%E0%AE%BF%E0%AE%B2%E0%AF%8D+%E0%AE%8E%E0%AE%A8%E0%AF%8D%E0%AE%A4+%E0%AE%B5%E0%AE%BF%E0%AE%B3%E0%AF%88%E0%AE%B5%E0%AE%BE%E0%AE%95+%E0%AE%87%E0%AE%A9%E0%AF%8D%E0%AE%AA%E0%AE%AE%E0%AF%8D+%E0%AE%86%E0%AE%A9%E0%AF%8D%E0%AE%B2%E0%AF%88%E0%AE%A9%E0%AF%8D+%E0%AE%AA%E0%AE%AF%E0%AE%A9%E0%AF%8D%E0%AE%AA%E0%AE%BE%E0%AE%9F%E0%AF%81%E0%AE%95%E0%AE%B3%E0%AF%8D+%E0%AE%87%E0%AE%B0%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95+%E0%AE%B5%E0%AF%87%E0%AE%A3%E0%AF%8D%E0%AE%9F%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%A4%E0%AE%AF%E0%AE%BE%E0%AE%B0%E0%AE%BF%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%A4%E0%AE%B5%E0%AE%B1%E0%AF%81+%E0%AE%95%E0%AE%A3%E0%AF%8D%E0%AE%9F%E0%AF%81%E0%AE%AA%E0%AE%BF%E0%AE%9F%E0%AE%BF%E0%AE%95%E0%AF%8D%E0%AE%95+%E0%AE%B5%E0%AE%B0%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%A8%E0%AE%BE%E0%AE%AE%E0%AF%8D+%E0%AE%A4%E0%AE%B1%E0%AF%8D%E0%AE%AA%E0%AF%87%E0%AE%BE%E0%AE%A4%E0%AF%81+%E0%AE%87%E0%AE%B0%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AE%BF%E0%AE%B1%E0%AF%87%E0%AE%BE%E0%AE%AE%E0%AF%8D.+%E0%AE%87%E0%AE%A8%E0%AF%8D%E0%AE%A4+%E0%AE%A8%E0%AE%BF%E0%AE%95%E0%AE%B4%E0%AF%8D%E0%AE%B5%E0%AF%81%E0%AE%95%E0%AE%B3%E0%AE%BF%E0%AE%B2%E0%AF%8D+%E0%AE%9A%E0%AF%86%E0%AE%AF%E0%AF%8D%E0%AE%A4%E0%AE%AA%E0%AE%BF%E0%AE%A9%E0%AF%8D+%E0%AE%85%E0%AE%AE%E0%AF%88%E0%AE%AA%E0%AF%8D%E0%AE%AA%E0%AE%BF%E0%AE%A9%E0%AF%8D+%E0%AE%95%E0%AE%A3%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AF%81%2C+%E0%AE%85%E0%AE%B5%E0%AE%B0%E0%AF%8D%E0%AE%95%E0%AE%B3%E0%AF%8D+%E0%AE%A4%E0%AE%B5%E0%AE%B1%E0%AF%81+%E0%AE%B5%E0%AE%BF%E0%AE%9F%E0%AF%8D%E0%AE%9F%E0%AF%81+quae+%E0%AE%AA%E0%AE%9F%E0%AF%8D%E0%AE%9F%E0%AE%B1%E0%AF%88+%E0%AE%A8%E0%AF%80%E0%AE%99%E0%AF%8D%E0%AE%95%E0%AE%B3%E0%AF%8D+%E0%AE%AA%E0%AE%B0%E0%AE%BF%E0%AE%A8%E0%AF%8D%E0%AE%A4%E0%AF%81%E0%AE%B0%E0%AF%88%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AE%BF%E0%AE%B1%E0%AF%87%E0%AE%BE%E0%AE%AE%E0%AF%8D+%E0%AE%AE%E0%AF%86%E0%AE%A9%E0%AF%8D%E0%AE%AE%E0%AF%88%E0%AE%AF%E0%AE%BE%E0%AE%95+%E0%AE%AE%E0%AE%BE%E0%AE%B1%E0%AF%81%E0%AE%AE%E0%AF%8D";
+    private static final String QUERY =
+            "%E0%AE%A8%E0%AE%BE%E0%AE%AE%E0%AF%8D+%E0%AE%AE%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AE%BF%E0%AE%AF%E0%AE%AE%E0%AE%BE%E0%AE%A9%2C+%E0%AE%9A%E0%AF%81%E0%AE%B5%E0%AE%BE%E0%AE%B0%E0%AE%B8%E0%AF%8D%E0%AE%AF%E0%AE%AE%E0%AE%BE%E0%AE%A9+%E0%AE%87%E0%AE%B0%E0%AF%81%E0%AE%AA%E0%AF%8D%E0%AE%AA%E0%AF%87%E0%AE%BE%E0%AE%AE%E0%AF%8D%2C+%E0%AE%86%E0%AE%A9%E0%AE%BE%E0%AE%B2%E0%AF%8D+%E0%AE%9A%E0%AE%BF%E0%AE%B2+%E0%AE%A8%E0%AF%87%E0%AE%B0%E0%AE%99%E0%AF%8D%E0%AE%95%E0%AE%B3%E0%AE%BF%E0%AE%B2%E0%AF%8D+%E0%AE%9A%E0%AF%82%E0%AE%B4%E0%AF%8D%E0%AE%A8%E0%AE%BF%E0%AE%B2%E0%AF%88+%E0%AE%8F%E0%AE%B1%E0%AF%8D%E0%AE%AA%E0%AE%9F%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%8E%E0%AE%A9%E0%AF%8D%E0%AE%AA%E0%AE%A4%E0%AE%BE%E0%AE%B2%E0%AF%8D+%E0%AE%AA%E0%AE%A3%E0%AE%BF%E0%AE%AF%E0%AF%88%E0%AE%AF%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%B5%E0%AE%B2%E0%AE%BF+%E0%AE%85%E0%AE%B5%E0%AE%B0%E0%AF%88+%E0%AE%9A%E0%AE%BF%E0%AE%B2+%E0%AE%AA%E0%AF%86%E0%AE%B0%E0%AE%BF%E0%AE%AF+%E0%AE%95%E0%AF%86%E0%AE%BE%E0%AE%B3%E0%AF%8D%E0%AE%AE%E0%AF%81%E0%AE%A4%E0%AE%B2%E0%AF%8D+%E0%AE%AE%E0%AF%81%E0%AE%9F%E0%AE%BF%E0%AE%AF%E0%AF%81%E0%AE%AE%E0%AF%8D.+%E0%AE%85%E0%AE%A4%E0%AF%81+%E0%AE%9A%E0%AE%BF%E0%AE%B2+%E0%AE%A8%E0%AE%A9%E0%AF%8D%E0%AE%AE%E0%AF%88%E0%AE%95%E0%AE%B3%E0%AF%88+%E0%AE%AA%E0%AF%86%E0%AE%B1+%E0%AE%A4%E0%AE%B5%E0%AE%BF%E0%AE%B0%2C+%E0%AE%8E%E0%AE%AA%E0%AF%8D%E0%AE%AA%E0%AF%87%E0%AE%BE%E0%AE%A4%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%89%E0%AE%B4%E0%AF%88%E0%AE%95%E0%AF%8D%E0%AE%95+%E0%AE%89%E0%AE%9F%E0%AE%B1%E0%AF%8D%E0%AE%AA%E0%AE%AF%E0%AE%BF%E0%AE%B1%E0%AF%8D%E0%AE%9A%E0%AE%BF+%E0%AE%AE%E0%AF%87%E0%AE%B1%E0%AF%8D%E0%AE%95%E0%AF%86%E0%AE%BE%E0%AE%B3%E0%AF%8D%E0%AE%95%E0%AE%BF%E0%AE%B1%E0%AE%A4%E0%AF%81+%E0%AE%8E%E0%AE%99%E0%AF%8D%E0%AE%95%E0%AE%B3%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AF%81+%E0%AE%87%E0%AE%A4%E0%AF%81+%E0%AE%92%E0%AE%B0%E0%AF%81+%E0%AE%9A%E0%AE%BF%E0%AE%B1%E0%AE%BF%E0%AE%AF+%E0%AE%89%E0%AE%A4%E0%AE%BE%E0%AE%B0%E0%AE%A3%E0%AE%AE%E0%AF%8D%2C+%E0%AE%8E%E0%AE%9F%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95.+%E0%AE%B0%E0%AE%AF%E0%AE%BF%E0%AE%B2%E0%AF%8D+%E0%AE%8E%E0%AE%A8%E0%AF%8D%E0%AE%A4+%E0%AE%B5%E0%AE%BF%E0%AE%B3%E0%AF%88%E0%AE%B5%E0%AE%BE%E0%AE%95+%E0%AE%87%E0%AE%A9%E0%AF%8D%E0%AE%AA%E0%AE%AE%E0%AF%8D+%E0%AE%86%E0%AE%A9%E0%AF%8D%E0%AE%B2%E0%AF%88%E0%AE%A9%E0%AF%8D+%E0%AE%AA%E0%AE%AF%E0%AE%A9%E0%AF%8D%E0%AE%AA%E0%AE%BE%E0%AE%9F%E0%AF%81%E0%AE%95%E0%AE%B3%E0%AF%8D+%E0%AE%87%E0%AE%B0%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95+%E0%AE%B5%E0%AF%87%E0%AE%A3%E0%AF%8D%E0%AE%9F%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%A4%E0%AE%AF%E0%AE%BE%E0%AE%B0%E0%AE%BF%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%A4%E0%AE%B5%E0%AE%B1%E0%AF%81+%E0%AE%95%E0%AE%A3%E0%AF%8D%E0%AE%9F%E0%AF%81%E0%AE%AA%E0%AE%BF%E0%AE%9F%E0%AE%BF%E0%AE%95%E0%AF%8D%E0%AE%95+%E0%AE%B5%E0%AE%B0%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%A8%E0%AE%BE%E0%AE%AE%E0%AF%8D+%E0%AE%A4%E0%AE%B1%E0%AF%8D%E0%AE%AA%E0%AF%87%E0%AE%BE%E0%AE%A4%E0%AF%81+%E0%AE%87%E0%AE%B0%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AE%BF%E0%AE%B1%E0%AF%87%E0%AE%BE%E0%AE%AE%E0%AF%8D.+%E0%AE%87%E0%AE%A8%E0%AF%8D%E0%AE%A4+%E0%AE%A8%E0%AE%BF%E0%AE%95%E0%AE%B4%E0%AF%8D%E0%AE%B5%E0%AF%81%E0%AE%95%E0%AE%B3%E0%AE%BF%E0%AE%B2%E0%AF%8D+%E0%AE%9A%E0%AF%86%E0%AE%AF%E0%AF%8D%E0%AE%A4%E0%AE%AA%E0%AE%BF%E0%AE%A9%E0%AF%8D+%E0%AE%85%E0%AE%AE%E0%AF%88%E0%AE%AA%E0%AF%8D%E0%AE%AA%E0%AE%BF%E0%AE%A9%E0%AF%8D+%E0%AE%95%E0%AE%A3%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AF%81%2C+%E0%AE%85%E0%AE%B5%E0%AE%B0%E0%AF%8D%E0%AE%95%E0%AE%B3%E0%AF%8D+%E0%AE%A4%E0%AE%B5%E0%AE%B1%E0%AF%81+%E0%AE%B5%E0%AE%BF%E0%AE%9F%E0%AF%8D%E0%AE%9F%E0%AF%81+quae+%E0%AE%AA%E0%AE%9F%E0%AF%8D%E0%AE%9F%E0%AE%B1%E0%AF%88+%E0%AE%A8%E0%AF%80%E0%AE%99%E0%AF%8D%E0%AE%95%E0%AE%B3%E0%AF%8D+%E0%AE%AA%E0%AE%B0%E0%AE%BF%E0%AE%A8%E0%AF%8D%E0%AE%A4%E0%AF%81%E0%AE%B0%E0%AF%88%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AE%BF%E0%AE%B1%E0%AF%87%E0%AE%BE%E0%AE%AE%E0%AF%8D+%E0%AE%AE%E0%AF%86%E0%AE%A9%E0%AF%8D%E0%AE%AE%E0%AF%88%E0%AE%AF%E0%AE%BE%E0%AE%95+%E0%AE%AE%E0%AE%BE%E0%AE%B1%E0%AF%81%E0%AE%AE%E0%AF%8D";
 
-    @Parameterized.Parameters(name = "mType({0})")
-    public static Collection cases() {
+    public static Collection getCases() {
         final List<Object[]> params = new ArrayList<>();
         for (Type type : Type.values()) {
-            params.add(new Object[]{type});
+            params.add(new Object[] {type});
         }
         return params;
     }
 
-    @Parameterized.Parameter(0)
-    public Type mType;
-
     Object mA1;
     Object mA2;
     Object mB1;
@@ -73,20 +74,13 @@
     Object mC1;
     Object mC2;
 
-    @Before
-    public void setUp() throws Exception {
-        mA1 = mType.newInstance("https://mail.google.com/mail/u/0/?shva=1#inbox");
-        mA2 = mType.newInstance("https://mail.google.com/mail/u/0/?shva=1#inbox");
-        mB1 = mType.newInstance("http://developer.android.com/reference/java/net/URI.html");
-        mB2 = mType.newInstance("http://developer.android.com/reference/java/net/URI.html");
-
-        mC1 = mType.newInstance("http://developer.android.com/query?q=" + QUERY);
-        // Replace the very last char.
-        mC2 = mType.newInstance("http://developer.android.com/query?q=" + QUERY.substring(0, QUERY.length() - 3) + "%AF");
-    }
-
     @Test
-    public void timeEquals() {
+    @Parameters(method = "getCases")
+    public void timeEquals(Type type) throws Exception {
+        mA1 = type.newInstance("https://mail.google.com/mail/u/0/?shva=1#inbox");
+        mA2 = type.newInstance("https://mail.google.com/mail/u/0/?shva=1#inbox");
+        mB1 = type.newInstance("http://developer.android.com/reference/java/net/URI.html");
+        mB2 = type.newInstance("http://developer.android.com/reference/java/net/URI.html");
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             mA1.equals(mB1);
@@ -96,7 +90,10 @@
     }
 
     @Test
-    public void timeHashCode() {
+    @Parameters(method = "getCases")
+    public void timeHashCode(Type type) throws Exception {
+        mA1 = type.newInstance("https://mail.google.com/mail/u/0/?shva=1#inbox");
+        mB1 = type.newInstance("http://developer.android.com/reference/java/net/URI.html");
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             mA1.hashCode();
@@ -105,7 +102,15 @@
     }
 
     @Test
-    public void timeEqualsWithHeavilyEscapedComponent() {
+    @Parameters(method = "getCases")
+    public void timeEqualsWithHeavilyEscapedComponent(Type type) throws Exception {
+        mC1 = type.newInstance("http://developer.android.com/query?q=" + QUERY);
+        // Replace the very last char.
+        mC2 =
+                type.newInstance(
+                        "http://developer.android.com/query?q="
+                                + QUERY.substring(0, QUERY.length() - 3)
+                                + "%AF");
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             mC1.equals(mC2);
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/KeyPairGeneratorPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/KeyPairGeneratorPerfTest.java
index 6fe9059..80c4487 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/KeyPairGeneratorPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/KeyPairGeneratorPerfTest.java
@@ -20,26 +20,24 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
-import org.junit.Before;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
-import java.security.SecureRandom;
 import java.util.Arrays;
 import java.util.Collection;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class KeyPairGeneratorPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameters(name = "mAlgorithm={0}, mImplementation={1}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(
                 new Object[][] {
                     {Algorithm.RSA, Implementation.BouncyCastle},
@@ -48,12 +46,6 @@
                 });
     }
 
-    @Parameterized.Parameter(0)
-    public Algorithm mAlgorithm;
-
-    @Parameterized.Parameter(1)
-    public Implementation mImplementation;
-
     public enum Algorithm {
         RSA,
         DSA,
@@ -66,26 +58,25 @@
 
     private String mGeneratorAlgorithm;
     private KeyPairGenerator mGenerator;
-    private SecureRandom mRandom;
 
-    @Before
-    public void setUp() throws Exception {
-        this.mGeneratorAlgorithm = mAlgorithm.toString();
+    public void setUp(Algorithm algorithm, Implementation implementation) throws Exception {
+        this.mGeneratorAlgorithm = algorithm.toString();
 
         final String provider;
-        if (mImplementation == Implementation.BouncyCastle) {
+        if (implementation == Implementation.BouncyCastle) {
             provider = "BC";
         } else {
             provider = "AndroidOpenSSL";
         }
 
         this.mGenerator = KeyPairGenerator.getInstance(mGeneratorAlgorithm, provider);
-        this.mRandom = SecureRandom.getInstance("SHA1PRNG");
         this.mGenerator.initialize(1024);
     }
 
     @Test
-    public void time() throws Exception {
+    @Parameters(method = "getData")
+    public void time(Algorithm algorithm, Implementation implementation) throws Exception {
+        setUp(algorithm, implementation);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             KeyPair keyPair = mGenerator.generateKeyPair();
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/LoopingBackwardsPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/LoopingBackwardsPerfTest.java
index 414764d..c9b0cbe 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/LoopingBackwardsPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/LoopingBackwardsPerfTest.java
@@ -20,11 +20,12 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -34,36 +35,34 @@
  *
  * @author Kevin Bourrillion
  */
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class LoopingBackwardsPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameters(name = "mMax={0}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(new Object[][] {{2}, {20}, {2000}, {20000000}});
     }
 
-    @Parameterized.Parameter(0)
-    public int mMax;
-
     @Test
-    public void timeForwards() {
+    @Parameters(method = "getData")
+    public void timeForwards(int max) {
         int fake = 0;
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            for (int j = 0; j < mMax; j++) {
+            for (int j = 0; j < max; j++) {
                 fake += j;
             }
         }
     }
 
     @Test
-    public void timeBackwards() {
+    @Parameters(method = "getData")
+    public void timeBackwards(int max) {
         int fake = 0;
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            for (int j = mMax - 1; j >= 0; j--) {
+            for (int j = max - 1; j >= 0; j--) {
                 fake += j;
             }
         }
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/MessageDigestPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/MessageDigestPerfTest.java
index 279681b..2dc947a 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/MessageDigestPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/MessageDigestPerfTest.java
@@ -20,24 +20,24 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.nio.ByteBuffer;
 import java.security.MessageDigest;
 import java.util.Arrays;
 import java.util.Collection;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class MessageDigestPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameters(name = "mAlgorithm={0}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(
                 new Object[][] {
                     {Algorithm.MD5},
@@ -48,9 +48,6 @@
                 });
     }
 
-    @Parameterized.Parameter(0)
-    public Algorithm mAlgorithm;
-
     public String mProvider = "AndroidOpenSSL";
 
     private static final int DATA_SIZE = 8192;
@@ -97,44 +94,44 @@
     };
 
     @Test
-    public void time() throws Exception {
+    @Parameters(method = "getData")
+    public void time(Algorithm algorithm) throws Exception {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            MessageDigest digest =
-                    MessageDigest.getInstance(mAlgorithm.toString(), mProvider);
+            MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider);
             digest.update(DATA, 0, DATA_SIZE);
             digest.digest();
         }
     }
 
     @Test
-    public void timeLargeArray() throws Exception {
+    @Parameters(method = "getData")
+    public void timeLargeArray(Algorithm algorithm) throws Exception {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            MessageDigest digest =
-                    MessageDigest.getInstance(mAlgorithm.toString(), mProvider);
+            MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider);
             digest.update(LARGE_DATA, 0, LARGE_DATA_SIZE);
             digest.digest();
         }
     }
 
     @Test
-    public void timeSmallChunkOfLargeArray() throws Exception {
+    @Parameters(method = "getData")
+    public void timeSmallChunkOfLargeArray(Algorithm algorithm) throws Exception {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            MessageDigest digest =
-                    MessageDigest.getInstance(mAlgorithm.toString(), mProvider);
+            MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider);
             digest.update(LARGE_DATA, LARGE_DATA_SIZE / 2, DATA_SIZE);
             digest.digest();
         }
     }
 
     @Test
-    public void timeSmallByteBuffer() throws Exception {
+    @Parameters(method = "getData")
+    public void timeSmallByteBuffer(Algorithm algorithm) throws Exception {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            MessageDigest digest =
-                    MessageDigest.getInstance(mAlgorithm.toString(), mProvider);
+            MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider);
             SMALL_BUFFER.position(0);
             SMALL_BUFFER.limit(SMALL_BUFFER.capacity());
             digest.update(SMALL_BUFFER);
@@ -143,11 +140,11 @@
     }
 
     @Test
-    public void timeSmallDirectByteBuffer() throws Exception {
+    @Parameters(method = "getData")
+    public void timeSmallDirectByteBuffer(Algorithm algorithm) throws Exception {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            MessageDigest digest =
-                    MessageDigest.getInstance(mAlgorithm.toString(), mProvider);
+            MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider);
             SMALL_DIRECT_BUFFER.position(0);
             SMALL_DIRECT_BUFFER.limit(SMALL_DIRECT_BUFFER.capacity());
             digest.update(SMALL_DIRECT_BUFFER);
@@ -156,11 +153,11 @@
     }
 
     @Test
-    public void timeLargeByteBuffer() throws Exception {
+    @Parameters(method = "getData")
+    public void timeLargeByteBuffer(Algorithm algorithm) throws Exception {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            MessageDigest digest =
-                    MessageDigest.getInstance(mAlgorithm.toString(), mProvider);
+            MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider);
             LARGE_BUFFER.position(0);
             LARGE_BUFFER.limit(LARGE_BUFFER.capacity());
             digest.update(LARGE_BUFFER);
@@ -169,11 +166,11 @@
     }
 
     @Test
-    public void timeLargeDirectByteBuffer() throws Exception {
+    @Parameters(method = "getData")
+    public void timeLargeDirectByteBuffer(Algorithm algorithm) throws Exception {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            MessageDigest digest =
-                    MessageDigest.getInstance(mAlgorithm.toString(), mProvider);
+            MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider);
             LARGE_DIRECT_BUFFER.position(0);
             LARGE_DIRECT_BUFFER.limit(LARGE_DIRECT_BUFFER.capacity());
             digest.update(LARGE_DIRECT_BUFFER);
@@ -182,11 +179,11 @@
     }
 
     @Test
-    public void timeSmallChunkOfLargeByteBuffer() throws Exception {
+    @Parameters(method = "getData")
+    public void timeSmallChunkOfLargeByteBuffer(Algorithm algorithm) throws Exception {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            MessageDigest digest =
-                    MessageDigest.getInstance(mAlgorithm.toString(), mProvider);
+            MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider);
             LARGE_BUFFER.position(LARGE_BUFFER.capacity() / 2);
             LARGE_BUFFER.limit(LARGE_BUFFER.position() + DATA_SIZE);
             digest.update(LARGE_BUFFER);
@@ -195,11 +192,11 @@
     }
 
     @Test
-    public void timeSmallChunkOfLargeDirectByteBuffer() throws Exception {
+    @Parameters(method = "getData")
+    public void timeSmallChunkOfLargeDirectByteBuffer(Algorithm algorithm) throws Exception {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            MessageDigest digest =
-                    MessageDigest.getInstance(mAlgorithm.toString(), mProvider);
+            MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider);
             LARGE_DIRECT_BUFFER.position(LARGE_DIRECT_BUFFER.capacity() / 2);
             LARGE_DIRECT_BUFFER.limit(LARGE_DIRECT_BUFFER.position() + DATA_SIZE);
             digest.update(LARGE_DIRECT_BUFFER);
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/MutableIntPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/MutableIntPerfTest.java
index 37bd73c..d9d4bb5 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/MutableIntPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/MutableIntPerfTest.java
@@ -20,17 +20,18 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.concurrent.atomic.AtomicInteger;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public final class MutableIntPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
@@ -96,29 +97,28 @@
         abstract int timeGet(BenchmarkState state);
     }
 
-    @Parameters(name = "mKind={0}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(new Object[][] {{Kind.ARRAY}, {Kind.ATOMIC}});
     }
 
-    @Parameterized.Parameter(0)
-    public Kind mKind;
-
     @Test
-    public void timeCreate() {
+    @Parameters(method = "getData")
+    public void timeCreate(Kind kind) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        mKind.timeCreate(state);
+        kind.timeCreate(state);
     }
 
     @Test
-    public void timeIncrement() {
+    @Parameters(method = "getData")
+    public void timeIncrement(Kind kind) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        mKind.timeIncrement(state);
+        kind.timeIncrement(state);
     }
 
     @Test
-    public void timeGet() {
+    @Parameters(method = "getData")
+    public void timeGet(Kind kind) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        mKind.timeGet(state);
+        kind.timeGet(state);
     }
 }
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/PriorityQueuePerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/PriorityQueuePerfTest.java
index 8801a56..48450b4 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/PriorityQueuePerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/PriorityQueuePerfTest.java
@@ -20,12 +20,12 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
-import org.junit.Before;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -35,13 +35,12 @@
 import java.util.PriorityQueue;
 import java.util.Random;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class PriorityQueuePerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameters(name = "mQueueSize={0}, mHitRate={1}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(
                 new Object[][] {
                     {100, 0},
@@ -62,26 +61,19 @@
                 });
     }
 
-    @Parameterized.Parameter(0)
-    public int mQueueSize;
-
-    @Parameterized.Parameter(1)
-    public int mHitRate;
-
     private PriorityQueue<Integer> mPq;
     private PriorityQueue<Integer> mUsepq;
     private List<Integer> mSeekElements;
     private Random mRandom = new Random(189279387L);
 
-    @Before
-    public void setUp() throws Exception {
+    public void setUp(int queueSize, int hitRate) throws Exception {
         mPq = new PriorityQueue<Integer>();
         mUsepq = new PriorityQueue<Integer>();
         mSeekElements = new ArrayList<Integer>();
         List<Integer> allElements = new ArrayList<Integer>();
-        int numShared = (int) (mQueueSize * ((double) mHitRate / 100));
-        // the total number of elements we require to engineer a hit rate of mHitRate%
-        int totalElements = 2 * mQueueSize - numShared;
+        int numShared = (int) (queueSize * ((double) hitRate / 100));
+        // the total number of elements we require to engineer a hit rate of hitRate%
+        int totalElements = 2 * queueSize - numShared;
         for (int i = 0; i < totalElements; i++) {
             allElements.add(i);
         }
@@ -93,11 +85,11 @@
             mSeekElements.add(allElements.get(i));
         }
         // add priority queue only elements (these won't be touched)
-        for (int i = numShared; i < mQueueSize; i++) {
+        for (int i = numShared; i < queueSize; i++) {
             mPq.add(allElements.get(i));
         }
         // add non-priority queue elements (these will be misses)
-        for (int i = mQueueSize; i < totalElements; i++) {
+        for (int i = queueSize; i < totalElements; i++) {
             mSeekElements.add(allElements.get(i));
         }
         mUsepq = new PriorityQueue<Integer>(mPq);
@@ -107,16 +99,18 @@
     }
 
     @Test
-    public void timeRemove() {
+    @Parameters(method = "getData")
+    public void timeRemove(int queueSize, int hitRate) throws Exception {
+        setUp(queueSize, hitRate);
         boolean fake = false;
         int elementsSize = mSeekElements.size();
         // At most allow the queue to empty 10%.
-        int resizingThreshold = mQueueSize / 10;
+        int resizingThreshold = queueSize / 10;
         int i = 0;
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             // Reset queue every so often. This will be called more often for smaller
-            // mQueueSizes, but since a copy is linear, it will also cost proportionally
+            // queueSizes, but since a copy is linear, it will also cost proportionally
             // less, and hopefully it will approximately balance out.
             if (++i % resizingThreshold == 0) {
                 mUsepq = new PriorityQueue<Integer>(mPq);
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/SchemePrefixPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/SchemePrefixPerfTest.java
index 42dc581..5ad62de 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/SchemePrefixPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/SchemePrefixPerfTest.java
@@ -20,11 +20,12 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -32,7 +33,7 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public final class SchemePrefixPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
@@ -85,19 +86,16 @@
         abstract String execute(String spec);
     }
 
-    @Parameters(name = "mStrategy={0}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(new Object[][] {{Strategy.REGEX}, {Strategy.JAVA}});
     }
 
-    @Parameterized.Parameter(0)
-    public Strategy mStrategy;
-
     @Test
-    public void timeSchemePrefix() {
+    @Parameters(method = "getData")
+    public void timeSchemePrefix(Strategy strategy) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mStrategy.execute("http://android.com");
+            strategy.execute("http://android.com");
         }
     }
 }
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/SignaturePerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/SignaturePerfTest.java
index 96e7cb2..a9a0788 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/SignaturePerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/SignaturePerfTest.java
@@ -19,12 +19,12 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
-import org.junit.Before;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
@@ -37,13 +37,12 @@
 import java.util.Map;
 
 /** Tests RSA and DSA mSignature creation and verification. */
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class SignaturePerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameters(name = "mAlgorithm={0}, mImplementation={1}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(
                 new Object[][] {
                     {Algorithm.MD5WithRSA, Implementation.OpenSSL},
@@ -55,12 +54,6 @@
                 });
     }
 
-    @Parameterized.Parameter(0)
-    public Algorithm mAlgorithm;
-
-    @Parameterized.Parameter(1)
-    public Implementation mImplementation;
-
     private static final int DATA_SIZE = 8192;
     private static final byte[] DATA = new byte[DATA_SIZE];
 
@@ -94,9 +87,8 @@
     private PrivateKey mPrivateKey;
     private PublicKey mPublicKey;
 
-    @Before
-    public void setUp() throws Exception {
-        this.mSignatureAlgorithm = mAlgorithm.toString();
+    public void setUp(Algorithm algorithm) throws Exception {
+        this.mSignatureAlgorithm = algorithm.toString();
 
         String keyAlgorithm =
                 mSignatureAlgorithm.substring(
@@ -121,11 +113,13 @@
     }
 
     @Test
-    public void timeSign() throws Exception {
+    @Parameters(method = "getData")
+    public void timeSign(Algorithm algorithm, Implementation implementation) throws Exception {
+        setUp(algorithm);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             Signature signer;
-            switch (mImplementation) {
+            switch (implementation) {
                 case OpenSSL:
                     signer = Signature.getInstance(mSignatureAlgorithm, "AndroidOpenSSL");
                     break;
@@ -133,7 +127,7 @@
                     signer = Signature.getInstance(mSignatureAlgorithm, "BC");
                     break;
                 default:
-                    throw new RuntimeException(mImplementation.toString());
+                    throw new RuntimeException(implementation.toString());
             }
             signer.initSign(mPrivateKey);
             signer.update(DATA);
@@ -142,11 +136,13 @@
     }
 
     @Test
-    public void timeVerify() throws Exception {
+    @Parameters(method = "getData")
+    public void timeVerify(Algorithm algorithm, Implementation implementation) throws Exception {
+        setUp(algorithm);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             Signature verifier;
-            switch (mImplementation) {
+            switch (implementation) {
                 case OpenSSL:
                     verifier = Signature.getInstance(mSignatureAlgorithm, "AndroidOpenSSL");
                     break;
@@ -154,7 +150,7 @@
                     verifier = Signature.getInstance(mSignatureAlgorithm, "BC");
                     break;
                 default:
-                    throw new RuntimeException(mImplementation.toString());
+                    throw new RuntimeException(implementation.toString());
             }
             verifier.initVerify(mPublicKey);
             verifier.update(DATA);
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/StringPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/StringPerfTest.java
index 02194b1..36db014 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/StringPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/StringPerfTest.java
@@ -20,16 +20,17 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.util.Arrays;
 import java.util.Collection;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class StringPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
@@ -46,8 +47,7 @@
         }
     }
 
-    @Parameters(name = "mStringLengths={0}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(
                 new Object[][] {
                     {StringLengths.EIGHT_KI},
@@ -57,9 +57,6 @@
                 });
     }
 
-    @Parameterized.Parameter(0)
-    public StringLengths mStringLengths;
-
     private static String makeString(int length) {
         StringBuilder result = new StringBuilder(length);
         for (int i = 0; i < length; ++i) {
@@ -69,10 +66,11 @@
     }
 
     @Test
-    public void timeHashCode() {
+    @Parameters(method = "getData")
+    public void timeHashCode(StringLengths stringLengths) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mStringLengths.mValue.hashCode();
+            stringLengths.mValue.hashCode();
         }
     }
 }
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/StringReplaceAllPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/StringReplaceAllPerfTest.java
index b0d1ee4..5b4423a 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/StringReplaceAllPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/StringReplaceAllPerfTest.java
@@ -20,16 +20,17 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.util.Arrays;
 import java.util.Collection;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class StringReplaceAllPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
@@ -69,8 +70,7 @@
         return stringBuilder.toString();
     }
 
-    @Parameters(name = "mStringLengths={0}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(
                 new Object[][] {
                     {StringLengths.BOOT_IMAGE},
@@ -82,30 +82,30 @@
                 });
     }
 
-    @Parameterized.Parameter(0)
-    public StringLengths mStringLengths;
-
     @Test
-    public void timeReplaceAllTrivialPatternNonExistent() {
+    @Parameters(method = "getData")
+    public void timeReplaceAllTrivialPatternNonExistent(StringLengths stringLengths) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mStringLengths.mValue.replaceAll("fish", "0");
+            stringLengths.mValue.replaceAll("fish", "0");
         }
     }
 
     @Test
-    public void timeReplaceTrivialPatternAllRepeated() {
+    @Parameters(method = "getData")
+    public void timeReplaceTrivialPatternAllRepeated(StringLengths stringLengths) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mStringLengths.mValue.replaceAll("jklm", "0");
+            stringLengths.mValue.replaceAll("jklm", "0");
         }
     }
 
     @Test
-    public void timeReplaceAllTrivialPatternSingleOccurrence() {
+    @Parameters(method = "getData")
+    public void timeReplaceAllTrivialPatternSingleOccurrence(StringLengths stringLengths) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mStringLengths.mValue.replaceAll("qrst", "0");
+            stringLengths.mValue.replaceAll("qrst", "0");
         }
     }
 }
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/StringReplacePerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/StringReplacePerfTest.java
index d2e657a..4d5c792 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/StringReplacePerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/StringReplacePerfTest.java
@@ -20,16 +20,17 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.util.Arrays;
 import java.util.Collection;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class StringReplacePerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
@@ -64,8 +65,7 @@
         return stringBuilder.toString();
     }
 
-    @Parameters(name = "mStringLengths={0}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(
                 new Object[][] {
                     {StringLengths.EMPTY},
@@ -76,54 +76,57 @@
                 });
     }
 
-    @Parameterized.Parameter(0)
-    public StringLengths mStringLengths;
-
     @Test
-    public void timeReplaceCharNonExistent() {
+    @Parameters(method = "getData")
+    public void timeReplaceCharNonExistent(StringLengths stringLengths) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mStringLengths.mValue.replace('z', '0');
+            stringLengths.mValue.replace('z', '0');
         }
     }
 
     @Test
-    public void timeReplaceCharRepeated() {
+    @Parameters(method = "getData")
+    public void timeReplaceCharRepeated(StringLengths stringLengths) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mStringLengths.mValue.replace('a', '0');
+            stringLengths.mValue.replace('a', '0');
         }
     }
 
     @Test
-    public void timeReplaceSingleChar() {
+    @Parameters(method = "getData")
+    public void timeReplaceSingleChar(StringLengths stringLengths) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mStringLengths.mValue.replace('q', '0');
+            stringLengths.mValue.replace('q', '0');
         }
     }
 
     @Test
-    public void timeReplaceSequenceNonExistent() {
+    @Parameters(method = "getData")
+    public void timeReplaceSequenceNonExistent(StringLengths stringLengths) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mStringLengths.mValue.replace("fish", "0");
+            stringLengths.mValue.replace("fish", "0");
         }
     }
 
     @Test
-    public void timeReplaceSequenceRepeated() {
+    @Parameters(method = "getData")
+    public void timeReplaceSequenceRepeated(StringLengths stringLengths) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mStringLengths.mValue.replace("jklm", "0");
+            stringLengths.mValue.replace("jklm", "0");
         }
     }
 
     @Test
-    public void timeReplaceSingleSequence() {
+    @Parameters(method = "getData")
+    public void timeReplaceSingleSequence(StringLengths stringLengths) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mStringLengths.mValue.replace("qrst", "0");
+            stringLengths.mValue.replace("qrst", "0");
         }
     }
 }
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/StringToBytesPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/StringToBytesPerfTest.java
index 1efc188..c004d95 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/StringToBytesPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/StringToBytesPerfTest.java
@@ -20,17 +20,18 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Collection;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class StringToBytesPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
@@ -53,8 +54,7 @@
         }
     }
 
-    @Parameters(name = "mStringLengths={0}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(
                 new Object[][] {
                     {StringLengths.EMPTY},
@@ -69,9 +69,6 @@
                 });
     }
 
-    @Parameterized.Parameter(0)
-    public StringLengths mStringLengths;
-
     private static String makeString(int length) {
         char[] chars = new char[length];
         for (int i = 0; i < length; ++i) {
@@ -89,26 +86,29 @@
     }
 
     @Test
-    public void timeGetBytesUtf8() {
+    @Parameters(method = "getData")
+    public void timeGetBytesUtf8(StringLengths stringLengths) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mStringLengths.mValue.getBytes(StandardCharsets.UTF_8);
+            stringLengths.mValue.getBytes(StandardCharsets.UTF_8);
         }
     }
 
     @Test
-    public void timeGetBytesIso88591() {
+    @Parameters(method = "getData")
+    public void timeGetBytesIso88591(StringLengths stringLengths) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mStringLengths.mValue.getBytes(StandardCharsets.ISO_8859_1);
+            stringLengths.mValue.getBytes(StandardCharsets.ISO_8859_1);
         }
     }
 
     @Test
-    public void timeGetBytesAscii() {
+    @Parameters(method = "getData")
+    public void timeGetBytesAscii(StringLengths stringLengths) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            mStringLengths.mValue.getBytes(StandardCharsets.US_ASCII);
+            stringLengths.mValue.getBytes(StandardCharsets.US_ASCII);
         }
     }
 }
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/StringToRealPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/StringToRealPerfTest.java
index b01948a..15516fc 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/StringToRealPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/StringToRealPerfTest.java
@@ -20,22 +20,22 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 
 import java.util.Arrays;
 import java.util.Collection;
 
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public class StringToRealPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameters(name = "mString={0}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(
                 new Object[][] {
                     {"NaN"},
@@ -49,22 +49,21 @@
                 });
     }
 
-    @Parameterized.Parameter(0)
-    public String mString;
-
     @Test
-    public void timeFloat_parseFloat() {
+    @Parameters(method = "getData")
+    public void timeFloat_parseFloat(String string) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            Float.parseFloat(mString);
+            Float.parseFloat(string);
         }
     }
 
     @Test
-    public void timeDouble_parseDouble() {
+    @Parameters(method = "getData")
+    public void timeDouble_parseDouble(String string) {
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
-            Double.parseDouble(mString);
+            Double.parseDouble(string);
         }
     }
 }
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/XMLEntitiesPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/XMLEntitiesPerfTest.java
index 2ea834d..ae1e8bc 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/XMLEntitiesPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/XMLEntitiesPerfTest.java
@@ -20,12 +20,12 @@
 import android.perftests.utils.PerfStatusReporter;
 import android.test.suitebuilder.annotation.LargeTest;
 
-import org.junit.Before;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
 import org.xml.sax.InputSource;
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserFactory;
@@ -38,13 +38,12 @@
 import javax.xml.parsers.DocumentBuilderFactory;
 
 // http://code.google.com/p/android/issues/detail?id=18102
-@RunWith(Parameterized.class)
+@RunWith(JUnitParamsRunner.class)
 @LargeTest
 public final class XMLEntitiesPerfTest {
     @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    @Parameters(name = "mLength={0}, mEntityFraction={1}")
-    public static Collection<Object[]> data() {
+    public static Collection<Object[]> getData() {
         return Arrays.asList(
                 new Object[][] {
                     {10, 0},
@@ -59,29 +58,22 @@
                 });
     }
 
-    @Parameterized.Parameter(0)
-    public int mLength;
-
-    @Parameterized.Parameter(1)
-    public float mEntityFraction;
-
     private XmlPullParserFactory mXmlPullParserFactory;
     private DocumentBuilderFactory mDocumentBuilderFactory;
 
     /** a string like {@code <doc>&amp;&amp;++</doc>}. */
     private String mXml;
 
-    @Before
-    public void setUp() throws Exception {
+    public void setUp(int length, float entityFraction) throws Exception {
         mXmlPullParserFactory = XmlPullParserFactory.newInstance();
         mDocumentBuilderFactory = DocumentBuilderFactory.newInstance();
 
         StringBuilder xmlBuilder = new StringBuilder();
         xmlBuilder.append("<doc>");
-        for (int i = 0; i < (mLength * mEntityFraction); i++) {
+        for (int i = 0; i < (length * entityFraction); i++) {
             xmlBuilder.append("&amp;");
         }
-        while (xmlBuilder.length() < mLength) {
+        while (xmlBuilder.length() < length) {
             xmlBuilder.append("+");
         }
         xmlBuilder.append("</doc>");
@@ -89,7 +81,9 @@
     }
 
     @Test
-    public void timeXmlParser() throws Exception {
+    @Parameters(method = "getData")
+    public void timeXmlParser(int length, float entityFraction) throws Exception {
+        setUp(length, entityFraction);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             XmlPullParser parser = mXmlPullParserFactory.newPullParser();
@@ -101,7 +95,9 @@
     }
 
     @Test
-    public void timeDocumentBuilder() throws Exception {
+    @Parameters(method = "getData")
+    public void timeDocumentBuilder(int length, float entityFraction) throws Exception {
+        setUp(length, entityFraction);
         BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             DocumentBuilder documentBuilder = mDocumentBuilderFactory.newDocumentBuilder();
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
index 20bca35..52dc01b 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -460,14 +460,14 @@
                     if (mPowerManager != null && mPowerManager.isDeviceIdleMode()) {
                         synchronized (mLock) {
                             stopUnexemptedJobsForDoze();
-                            stopLongRunningJobsLocked("deep doze");
+                            stopOvertimeJobsLocked("deep doze");
                         }
                     }
                     break;
                 case PowerManager.ACTION_POWER_SAVE_MODE_CHANGED:
                     if (mPowerManager != null && mPowerManager.isPowerSaveMode()) {
                         synchronized (mLock) {
-                            stopLongRunningJobsLocked("battery saver");
+                            stopOvertimeJobsLocked("battery saver");
                         }
                     }
                     break;
@@ -555,7 +555,7 @@
      * execution guarantee.
      */
     @GuardedBy("mLock")
-    boolean isJobLongRunningLocked(@NonNull JobStatus job) {
+    boolean isJobInOvertimeLocked(@NonNull JobStatus job) {
         if (!mRunningJobs.contains(job)) {
             return false;
         }
@@ -1043,7 +1043,7 @@
     }
 
     @GuardedBy("mLock")
-    private void stopLongRunningJobsLocked(@NonNull String debugReason) {
+    private void stopOvertimeJobsLocked(@NonNull String debugReason) {
         for (int i = 0; i < mActiveServices.size(); ++i) {
             final JobServiceContext jsc = mActiveServices.get(i);
             final JobStatus jobStatus = jsc.getRunningJobLocked();
@@ -1060,7 +1060,7 @@
      * restricted by the given {@link JobRestriction}.
      */
     @GuardedBy("mLock")
-    void maybeStopLongRunningJobsLocked(@NonNull JobRestriction restriction) {
+    void maybeStopOvertimeJobsLocked(@NonNull JobRestriction restriction) {
         for (int i = mActiveServices.size() - 1; i >= 0; --i) {
             final JobServiceContext jsc = mActiveServices.get(i);
             final JobStatus jobStatus = jsc.getRunningJobLocked();
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index d28ebde..6375d0d 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -1870,10 +1870,10 @@
         return mConcurrencyManager.isJobRunningLocked(job);
     }
 
-    /** @see JobConcurrencyManager#isJobLongRunningLocked(JobStatus) */
+    /** @see JobConcurrencyManager#isJobInOvertimeLocked(JobStatus) */
     @GuardedBy("mLock")
-    public boolean isLongRunningLocked(JobStatus job) {
-        return mConcurrencyManager.isJobLongRunningLocked(job);
+    public boolean isJobInOvertimeLocked(JobStatus job) {
+        return mConcurrencyManager.isJobInOvertimeLocked(job);
     }
 
     private void noteJobPending(JobStatus job) {
@@ -2155,11 +2155,11 @@
 
     @Override
     public void onRestrictionStateChanged(@NonNull JobRestriction restriction,
-            boolean stopLongRunningJobs) {
+            boolean stopOvertimeJobs) {
         mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
-        if (stopLongRunningJobs) {
+        if (stopOvertimeJobs) {
             synchronized (mLock) {
-                mConcurrencyManager.maybeStopLongRunningJobsLocked(restriction);
+                mConcurrencyManager.maybeStopOvertimeJobsLocked(restriction);
             }
         }
     }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java b/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java
index d7bd030..554f152 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java
@@ -41,11 +41,11 @@
      * Called by a {@link com.android.server.job.restrictions.JobRestriction} to notify the
      * JobScheduler that it should check on the state of all jobs.
      *
-     * @param stopLongRunningJobs Whether to stop any jobs that have run for more than their minimum
-     *                            execution guarantee and are restricted by the changed restriction
+     * @param stopOvertimeJobs Whether to stop any jobs that have run for more than their minimum
+     *                         execution guarantee and are restricted by the changed restriction
      */
     void onRestrictionStateChanged(@NonNull JobRestriction restriction,
-            boolean stopLongRunningJobs);
+            boolean stopOvertimeJobs);
 
     /**
      * Called by the controller to notify the JobManager that regardless of the state of the task,
diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
index a007a69..ca2fd60 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
@@ -90,11 +90,11 @@
         final int priority = job.getEffectivePriority();
         if (mThermalStatus >= HIGHER_PRIORITY_THRESHOLD) {
             // For moderate throttling, only let expedited jobs and high priority regular jobs that
-            // haven't been running for long run.
+            // haven't been running for a long time run.
             return !job.shouldTreatAsExpeditedJob()
                     && !(priority == JobInfo.PRIORITY_HIGH
                         && mService.isCurrentlyRunningLocked(job)
-                        && !mService.isLongRunningLocked(job));
+                        && !mService.isJobInOvertimeLocked(job));
         }
         if (mThermalStatus >= LOW_PRIORITY_THRESHOLD) {
             // For light throttling, throttle all min priority jobs and all low priority jobs that
@@ -102,7 +102,7 @@
             return priority == JobInfo.PRIORITY_MIN
                     || (priority == JobInfo.PRIORITY_LOW
                         && (!mService.isCurrentlyRunningLocked(job)
-                            || mService.isLongRunningLocked(job)));
+                            || mService.isJobInOvertimeLocked(job)));
         }
         return false;
     }
diff --git a/core/api/current.txt b/core/api/current.txt
index 40e0c5b..218d7bd 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -172,6 +172,7 @@
     field public static final String REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE = "android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE";
     field public static final String REQUEST_PASSWORD_COMPLEXITY = "android.permission.REQUEST_PASSWORD_COMPLEXITY";
     field @Deprecated public static final String RESTART_PACKAGES = "android.permission.RESTART_PACKAGES";
+    field public static final String RUN_LONG_JOBS = "android.permission.RUN_LONG_JOBS";
     field public static final String SCHEDULE_EXACT_ALARM = "android.permission.SCHEDULE_EXACT_ALARM";
     field public static final String SEND_RESPOND_VIA_MESSAGE = "android.permission.SEND_RESPOND_VIA_MESSAGE";
     field public static final String SEND_SMS = "android.permission.SEND_SMS";
@@ -19497,7 +19498,7 @@
     method public boolean hasSatelliteBlocklist();
     method public boolean hasSatellitePvt();
     method public boolean hasScheduling();
-    method public boolean hasSingleShot();
+    method public boolean hasSingleShotFix();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.location.GnssCapabilities> CREATOR;
   }
@@ -19529,7 +19530,7 @@
     method @NonNull public android.location.GnssCapabilities.Builder setHasSatelliteBlocklist(boolean);
     method @NonNull public android.location.GnssCapabilities.Builder setHasSatellitePvt(boolean);
     method @NonNull public android.location.GnssCapabilities.Builder setHasScheduling(boolean);
-    method @NonNull public android.location.GnssCapabilities.Builder setHasSingleShot(boolean);
+    method @NonNull public android.location.GnssCapabilities.Builder setHasSingleShotFix(boolean);
   }
 
   public final class GnssClock implements android.os.Parcelable {
@@ -19727,7 +19728,7 @@
     method public int getConstellationType(@IntRange(from=0) int);
     method @FloatRange(from=0xffffffa6, to=90) public float getElevationDegrees(@IntRange(from=0) int);
     method @IntRange(from=0) public int getSatelliteCount();
-    method @IntRange(from=1, to=200) public int getSvid(@IntRange(from=0) int);
+    method @IntRange(from=1, to=206) public int getSvid(@IntRange(from=0) int);
     method public boolean hasAlmanacData(@IntRange(from=0) int);
     method public boolean hasBasebandCn0DbHz(@IntRange(from=0) int);
     method public boolean hasCarrierFrequencyHz(@IntRange(from=0) int);
@@ -44006,9 +44007,10 @@
     field public static final long NETWORK_TYPE_BITMASK_HSPA = 512L; // 0x200L
     field public static final long NETWORK_TYPE_BITMASK_HSPAP = 16384L; // 0x4000L
     field public static final long NETWORK_TYPE_BITMASK_HSUPA = 256L; // 0x100L
+    field public static final long NETWORK_TYPE_BITMASK_IDEN = 1024L; // 0x400L
     field public static final long NETWORK_TYPE_BITMASK_IWLAN = 131072L; // 0x20000L
     field public static final long NETWORK_TYPE_BITMASK_LTE = 4096L; // 0x1000L
-    field public static final long NETWORK_TYPE_BITMASK_LTE_CA = 262144L; // 0x40000L
+    field @Deprecated public static final long NETWORK_TYPE_BITMASK_LTE_CA = 262144L; // 0x40000L
     field public static final long NETWORK_TYPE_BITMASK_NR = 524288L; // 0x80000L
     field public static final long NETWORK_TYPE_BITMASK_TD_SCDMA = 65536L; // 0x10000L
     field public static final long NETWORK_TYPE_BITMASK_UMTS = 4L; // 0x4L
@@ -47431,6 +47433,7 @@
     field public static final int DENSITY_420 = 420; // 0x1a4
     field public static final int DENSITY_440 = 440; // 0x1b8
     field public static final int DENSITY_450 = 450; // 0x1c2
+    field public static final int DENSITY_520 = 520; // 0x208
     field public static final int DENSITY_560 = 560; // 0x230
     field public static final int DENSITY_600 = 600; // 0x258
     field public static final int DENSITY_DEFAULT = 160; // 0xa0
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index e890005..88efcce 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -301,10 +301,6 @@
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void reportNetworkInterfaceForTransports(@NonNull String, @NonNull int[]) throws java.lang.RuntimeException;
   }
 
-  public class Binder implements android.os.IBinder {
-    method public final void markVintfStability();
-  }
-
   public class BluetoothServiceManager {
     method @NonNull public android.os.BluetoothServiceManager.ServiceRegisterer getBluetoothManagerServiceRegisterer();
   }
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 74288ae..a382ecf 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -9351,6 +9351,7 @@
 
   public class Binder implements android.os.IBinder {
     method public int handleShellCommand(@NonNull android.os.ParcelFileDescriptor, @NonNull android.os.ParcelFileDescriptor, @NonNull android.os.ParcelFileDescriptor, @NonNull String[]);
+    method public final void markVintfStability();
     method public static void setProxyTransactListener(@Nullable android.os.Binder.ProxyTransactListener);
   }
 
@@ -13397,7 +13398,7 @@
     method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int[] getCompleteActiveSubscriptionIdList();
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEnabledSubscriptionId(int);
     method @NonNull public static android.content.res.Resources getResourcesForSubId(@NonNull android.content.Context, int);
-    method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public android.os.UserHandle getUserHandle(int);
+    method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public android.os.UserHandle getSubscriptionUserHandle(int);
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isSubscriptionEnabled(int);
     method public void requestEmbeddedSubscriptionInfoListRefresh();
     method public void requestEmbeddedSubscriptionInfoListRefresh(int);
@@ -13407,8 +13408,8 @@
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDefaultVoiceSubscriptionId(int);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setPreferredDataSubscriptionId(int, boolean, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.Consumer<java.lang.Integer>);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setSubscriptionEnabled(int, boolean);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public void setSubscriptionUserHandle(int, @Nullable android.os.UserHandle);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setUiccApplicationsEnabled(int, boolean);
-    method @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public void setUserHandle(int, @Nullable android.os.UserHandle);
     field @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_PLANS) public static final String ACTION_SUBSCRIPTION_PLANS_CHANGED = "android.telephony.action.SUBSCRIPTION_PLANS_CHANGED";
     field @NonNull public static final android.net.Uri ADVANCED_CALLING_ENABLED_CONTENT_URI;
     field @NonNull public static final android.net.Uri CROSS_SIM_ENABLED_CONTENT_URI;
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index d1275f6..1b972e0 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1353,9 +1353,16 @@
     public static final int OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO =
             AppProtoEnums.APP_OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO;
 
+    /**
+     * App can schedule long running jobs.
+     *
+     * @hide
+     */
+    public static final int OP_RUN_LONG_JOBS = AppProtoEnums.APP_OP_RUN_LONG_JOBS;
+
     /** @hide */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public static final int _NUM_OP = 122;
+    public static final int _NUM_OP = 123;
 
     /** Access to coarse location information. */
     public static final String OPSTR_COARSE_LOCATION = "android:coarse_location";
@@ -1839,6 +1846,13 @@
     public static final String OPSTR_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO =
             "android:receive_explicit_user_interaction_audio";
 
+    /**
+     * App can schedule long running jobs.
+     *
+     * @hide
+     */
+    public static final String OPSTR_RUN_LONG_JOBS = "android:run_long_jobs";
+
     /** {@link #sAppOpsToNote} not initialized yet for this op */
     private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0;
     /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */
@@ -1933,6 +1947,7 @@
             OP_SCHEDULE_EXACT_ALARM,
             OP_MANAGE_MEDIA,
             OP_TURN_SCREEN_ON,
+            OP_RUN_LONG_JOBS,
     };
 
     static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{
@@ -2312,7 +2327,9 @@
         new AppOpInfo.Builder(OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO,
                 OPSTR_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO,
                 "RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO").setDefaultMode(
-                AppOpsManager.MODE_ALLOWED).build()
+                AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_RUN_LONG_JOBS, OPSTR_RUN_LONG_JOBS, "RUN_LONG_JOBS")
+                .setPermission(Manifest.permission.RUN_LONG_JOBS).build()
     };
 
     /**
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 08a6b8c..aaa3d21 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -173,6 +173,7 @@
 import android.os.IUserManager;
 import android.os.IncidentManager;
 import android.os.PerformanceHintManager;
+import android.os.PermissionEnforcer;
 import android.os.PowerManager;
 import android.os.RecoverySystem;
 import android.os.ServiceManager;
@@ -1366,6 +1367,14 @@
                         return new PermissionCheckerManager(ctx.getOuterContext());
                     }});
 
+        registerService(Context.PERMISSION_ENFORCER_SERVICE, PermissionEnforcer.class,
+                new CachedServiceFetcher<PermissionEnforcer>() {
+                    @Override
+                    public PermissionEnforcer createService(ContextImpl ctx)
+                            throws ServiceNotFoundException {
+                        return new PermissionEnforcer(ctx.getOuterContext());
+                    }});
+
         registerService(Context.DYNAMIC_SYSTEM_SERVICE, DynamicSystemManager.class,
                 new CachedServiceFetcher<DynamicSystemManager>() {
                     @Override
diff --git a/core/java/android/app/backup/BackupRestoreEventLogger.java b/core/java/android/app/backup/BackupRestoreEventLogger.java
index b789b38..760c6f0 100644
--- a/core/java/android/app/backup/BackupRestoreEventLogger.java
+++ b/core/java/android/app/backup/BackupRestoreEventLogger.java
@@ -19,10 +19,15 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.util.Slog;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.Collections;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -38,6 +43,8 @@
  * @hide
  */
 public class BackupRestoreEventLogger {
+    private static final String TAG = "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.
@@ -72,6 +79,8 @@
     public @interface BackupRestoreError {}
 
     private final int mOperationType;
+    private final Map<String, DataTypeResult> mResults = new HashMap<>();
+    private final MessageDigest mHashDigest;
 
     /**
      * @param operationType type of the operation for which logging will be performed. See
@@ -81,6 +90,14 @@
      */
     public BackupRestoreEventLogger(@OperationType int operationType) {
         mOperationType = operationType;
+
+        MessageDigest hashDigest = null;
+        try {
+            hashDigest = MessageDigest.getInstance("SHA-256");
+        } catch (NoSuchAlgorithmException e) {
+            Slog.w("Couldn't create MessageDigest for hash computation", e);
+        }
+        mHashDigest = hashDigest;
     }
 
     /**
@@ -98,7 +115,7 @@
      * @return boolean, indicating whether the log has been accepted.
      */
     public boolean logItemsBackedUp(@NonNull @BackupRestoreDataType String dataType, int count) {
-        return true;
+        return logSuccess(OperationType.BACKUP, dataType, count);
     }
 
     /**
@@ -118,7 +135,7 @@
      */
     public boolean logItemsBackupFailed(@NonNull @BackupRestoreDataType String dataType, int count,
             @Nullable @BackupRestoreError String error) {
-        return true;
+        return logFailure(OperationType.BACKUP, dataType, count, error);
     }
 
     /**
@@ -139,7 +156,7 @@
      */
     public boolean logBackupMetaData(@NonNull @BackupRestoreDataType String dataType,
             @NonNull String metaData) {
-        return true;
+        return logMetaData(OperationType.BACKUP, dataType, metaData);
     }
 
     /**
@@ -159,7 +176,7 @@
      * @return boolean, indicating whether the log has been accepted.
      */
     public boolean logItemsRestored(@NonNull @BackupRestoreDataType String dataType, int count) {
-        return true;
+        return logSuccess(OperationType.RESTORE, dataType, count);
     }
 
     /**
@@ -181,7 +198,7 @@
      */
     public boolean logItemsRestoreFailed(@NonNull @BackupRestoreDataType String dataType, int count,
             @Nullable @BackupRestoreError String error) {
-        return true;
+        return logFailure(OperationType.RESTORE, dataType, count, error);
     }
 
     /**
@@ -204,7 +221,7 @@
      */
     public boolean logRestoreMetadata(@NonNull @BackupRestoreDataType String dataType,
             @NonNull  String metadata) {
-        return true;
+        return logMetaData(OperationType.RESTORE, dataType, metadata);
     }
 
     /**
@@ -214,7 +231,7 @@
      * @hide
      */
     public List<DataTypeResult> getLoggingResults() {
-        return Collections.emptyList();
+        return new ArrayList<>(mResults.values());
     }
 
     /**
@@ -227,22 +244,97 @@
         return mOperationType;
     }
 
+    private boolean logSuccess(@OperationType int operationType,
+            @BackupRestoreDataType String dataType, int count) {
+        DataTypeResult dataTypeResult = getDataTypeResult(operationType, dataType);
+        if (dataTypeResult == null) {
+            return false;
+        }
+
+        dataTypeResult.mSuccessCount += count;
+        mResults.put(dataType, dataTypeResult);
+
+        return true;
+    }
+
+    private boolean logFailure(@OperationType int operationType,
+            @NonNull @BackupRestoreDataType String dataType, int count,
+            @Nullable @BackupRestoreError String error) {
+        DataTypeResult dataTypeResult = getDataTypeResult(operationType, dataType);
+        if (dataTypeResult == null) {
+            return false;
+        }
+
+        dataTypeResult.mFailCount += count;
+        if (error != null) {
+            dataTypeResult.mErrors.merge(error, count, Integer::sum);
+        }
+
+        return true;
+    }
+
+    private boolean logMetaData(@OperationType int operationType,
+            @NonNull @BackupRestoreDataType String dataType, @NonNull String metaData) {
+        if (mHashDigest == null) {
+            return false;
+        }
+        DataTypeResult dataTypeResult = getDataTypeResult(operationType, dataType);
+        if (dataTypeResult == null) {
+            return false;
+        }
+
+        dataTypeResult.mMetadataHash = getMetaDataHash(metaData);
+
+        return true;
+    }
+
+    /**
+     * Get the result container for the given data type.
+     *
+     * @return {@code DataTypeResult} object corresponding to the given {@code dataType} or
+     *         {@code null} if the logger can't accept logs for the given data type.
+     */
+    @Nullable
+    private DataTypeResult getDataTypeResult(@OperationType int operationType,
+            @BackupRestoreDataType String dataType) {
+        if (operationType != mOperationType) {
+            // Operation type for which we're trying to record logs doesn't match the operation
+            // type for which this logger instance was created.
+            Slog.d(TAG, "Operation type mismatch: logger created for " + mOperationType
+                    + ", trying to log for " + operationType);
+            return null;
+        }
+
+        if (!mResults.containsKey(dataType)) {
+            if (mResults.keySet().size() == DATA_TYPES_ALLOWED) {
+                // This is a new data type and we're already at capacity.
+                Slog.d(TAG, "Logger is full, ignoring new data type");
+                return null;
+            }
+
+            mResults.put(dataType,  new DataTypeResult(dataType));
+        }
+
+        return mResults.get(dataType);
+    }
+
+    private byte[] getMetaDataHash(String metaData) {
+        return mHashDigest.digest(metaData.getBytes(StandardCharsets.UTF_8));
+    }
+
     /**
      * 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;
+        private int mSuccessCount;
+        private int mFailCount;
+        private final Map<String, Integer> mErrors = new HashMap<>();
+        private byte[] mMetadataHash;
 
-        public DataTypeResult(String dataType, int successCount,
-                Map<String, Integer> errors, byte[] metadataHash) {
+        public DataTypeResult(String dataType) {
             mDataType = dataType;
-            mSuccessCount = successCount;
-            mErrors = errors;
-            mMetadataHash = metadataHash;
         }
 
         @NonNull
@@ -260,6 +352,13 @@
         }
 
         /**
+         * @return number of items of the given data type that have failed to back up or restore.
+         */
+        public int getFailCount() {
+            return mFailCount;
+        }
+
+        /**
          * @return mapping of {@link BackupRestoreError} to the count of items that are affected by
          *         the error.
          */
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index d65210b..1df0fa8 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -5142,6 +5142,14 @@
     public static final String PERMISSION_CHECKER_SERVICE = "permission_checker";
 
     /**
+     * Official published name of the (internal) permission enforcer service.
+     *
+     * @see #getSystemService(String)
+     * @hide
+     */
+    public static final String PERMISSION_ENFORCER_SERVICE = "permission_enforcer";
+
+    /**
      * Use with {@link #getSystemService(String) to retrieve an
      * {@link android.apphibernation.AppHibernationManager}} for
      * communicating with the hibernation service.
@@ -5194,6 +5202,15 @@
     public static final String DROPBOX_SERVICE = "dropbox";
 
     /**
+     * System service name for BackgroundInstallControlService. This service supervises the MBAs
+     * on device and provides the related metadata of the MBAs.
+     *
+     * @hide
+     */
+    @SuppressLint("ServiceName")
+    public static final String BACKGROUND_INSTALL_CONTROL_SERVICE = "background_install_control";
+
+    /**
      * System service name for BinaryTransparencyService. This is used to retrieve measurements
      * pertaining to various pre-installed and system binaries on device for the purposes of
      * providing transparency to the user.
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt b/core/java/android/content/pm/IBackgroundInstallControlService.aidl
similarity index 72%
copy from packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt
copy to core/java/android/content/pm/IBackgroundInstallControlService.aidl
index 581dafa3..c8e7cae 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt
+++ b/core/java/android/content/pm/IBackgroundInstallControlService.aidl
@@ -14,10 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.systemui.keyguard.domain.model
+package android.content.pm;
 
-/** Enumerates all possible positions for quick affordances that can appear on the lock-screen. */
-enum class KeyguardQuickAffordancePosition {
-    BOTTOM_START,
-    BOTTOM_END,
+import android.content.pm.ParceledListSlice;
+
+/**
+ * {@hide}
+ */
+interface IBackgroundInstallControlService {
+    ParceledListSlice getBackgroundInstalledPackages(long flags, int userId);
 }
diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java
index b9cef0f..30ee118 100644
--- a/core/java/android/credentials/CredentialManager.java
+++ b/core/java/android/credentials/CredentialManager.java
@@ -85,7 +85,7 @@
         ICancellationSignal cancelRemote = null;
         try {
             cancelRemote = mService.executeGetCredential(request,
-                    new GetCredentialTransport(executor, callback));
+                    new GetCredentialTransport(executor, callback), mContext.getOpPackageName());
         } catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
@@ -124,7 +124,8 @@
         ICancellationSignal cancelRemote = null;
         try {
             cancelRemote = mService.executeCreateCredential(request,
-                    new CreateCredentialTransport(executor, callback));
+                    new CreateCredentialTransport(executor, callback),
+                    mContext.getOpPackageName());
         } catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/credentials/ICredentialManager.aidl b/core/java/android/credentials/ICredentialManager.aidl
index dcf7106..b0f27f9 100644
--- a/core/java/android/credentials/ICredentialManager.aidl
+++ b/core/java/android/credentials/ICredentialManager.aidl
@@ -29,7 +29,7 @@
  */
 interface ICredentialManager {
 
-    @nullable ICancellationSignal executeGetCredential(in GetCredentialRequest request, in IGetCredentialCallback callback);
+    @nullable ICancellationSignal executeGetCredential(in GetCredentialRequest request, in IGetCredentialCallback callback, String callingPackage);
 
-    @nullable ICancellationSignal executeCreateCredential(in CreateCredentialRequest request, in ICreateCredentialCallback callback);
+    @nullable ICancellationSignal executeCreateCredential(in CreateCredentialRequest request, in ICreateCredentialCallback callback, String callingPackage);
 }
diff --git a/core/java/android/credentials/ui/ProviderData.java b/core/java/android/credentials/ui/ProviderData.java
index 35e12fa..3728469 100644
--- a/core/java/android/credentials/ui/ProviderData.java
+++ b/core/java/android/credentials/ui/ProviderData.java
@@ -43,10 +43,10 @@
             "android.credentials.ui.extra.PROVIDER_DATA_LIST";
 
     @NonNull
-    private final String mProviderId;
+    private final String mProviderFlattenedComponentName;
     @NonNull
     private final String mProviderDisplayName;
-    @NonNull
+    @Nullable
     private final Icon mIcon;
     @NonNull
     private final List<Entry> mCredentialEntries;
@@ -58,11 +58,11 @@
     private final @CurrentTimeMillisLong long mLastUsedTimeMillis;
 
     public ProviderData(
-            @NonNull String providerId, @NonNull String providerDisplayName,
-            @NonNull Icon icon, @NonNull List<Entry> credentialEntries,
+            @NonNull String providerFlattenedComponentName, @NonNull String providerDisplayName,
+            @Nullable Icon icon, @NonNull List<Entry> credentialEntries,
             @NonNull List<Entry> actionChips, @Nullable Entry authenticationEntry,
             @CurrentTimeMillisLong long lastUsedTimeMillis) {
-        mProviderId = providerId;
+        mProviderFlattenedComponentName = providerFlattenedComponentName;
         mProviderDisplayName = providerDisplayName;
         mIcon = icon;
         mCredentialEntries = credentialEntries;
@@ -73,8 +73,8 @@
 
     /** Returns the unique provider id. */
     @NonNull
-    public String getProviderId() {
-        return mProviderId;
+    public String getProviderFlattenedComponentName() {
+        return mProviderFlattenedComponentName;
     }
 
     @NonNull
@@ -82,7 +82,7 @@
         return mProviderDisplayName;
     }
 
-    @NonNull
+    @Nullable
     public Icon getIcon() {
         return mIcon;
     }
@@ -108,9 +108,9 @@
     }
 
     protected ProviderData(@NonNull Parcel in) {
-        String providerId = in.readString8();
-        mProviderId = providerId;
-        AnnotationValidations.validate(NonNull.class, null, mProviderId);
+        String providerFlattenedComponentName = in.readString8();
+        mProviderFlattenedComponentName = providerFlattenedComponentName;
+        AnnotationValidations.validate(NonNull.class, null, mProviderFlattenedComponentName);
 
         String providerDisplayName = in.readString8();
         mProviderDisplayName = providerDisplayName;
@@ -118,7 +118,6 @@
 
         Icon icon = in.readTypedObject(Icon.CREATOR);
         mIcon = icon;
-        AnnotationValidations.validate(NonNull.class, null, mIcon);
 
         List<Entry> credentialEntries = new ArrayList<>();
         in.readTypedList(credentialEntries, Entry.CREATOR);
@@ -139,7 +138,7 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeString8(mProviderId);
+        dest.writeString8(mProviderFlattenedComponentName);
         dest.writeString8(mProviderDisplayName);
         dest.writeTypedObject(mIcon, flags);
         dest.writeTypedList(mCredentialEntries);
@@ -171,26 +170,27 @@
      * @hide
      */
     public static class Builder {
-        private @NonNull String mProviderId;
+        private @NonNull String mProviderFlattenedComponentName;
         private @NonNull String mProviderDisplayName;
-        private @NonNull Icon mIcon;
+        private @Nullable 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;
+        public Builder(@NonNull String providerFlattenedComponentName,
+                @NonNull String providerDisplayName,
+                @Nullable Icon icon) {
+            mProviderFlattenedComponentName = providerFlattenedComponentName;
             mProviderDisplayName = providerDisplayName;
             mIcon = icon;
         }
 
         /** Sets the unique provider id. */
         @NonNull
-        public Builder setProviderId(@NonNull String providerId) {
-            mProviderId = providerId;
+        public Builder setProviderFlattenedComponentName(@NonNull String providerFlattenedComponentName) {
+            mProviderFlattenedComponentName = providerFlattenedComponentName;
             return this;
         }
 
@@ -239,7 +239,8 @@
         /** Builds a {@link ProviderData}. */
         @NonNull
         public ProviderData build() {
-            return new ProviderData(mProviderId, mProviderDisplayName, mIcon, mCredentialEntries,
+            return new ProviderData(mProviderFlattenedComponentName, mProviderDisplayName,
+                    mIcon, mCredentialEntries,
                 mActionChips, mAuthenticationEntry, mLastUsedTimeMillis);
         }
     }
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index 5403f08..3c73eb6 100644
--- a/core/java/android/hardware/fingerprint/FingerprintManager.java
+++ b/core/java/android/hardware/fingerprint/FingerprintManager.java
@@ -918,6 +918,22 @@
         }
     }
 
+    /**
+     * @hide
+     */
+    @RequiresPermission(USE_BIOMETRIC_INTERNAL)
+    public void setUdfpsOverlay(@NonNull IUdfpsOverlay controller) {
+        if (mService == null) {
+            Slog.w(TAG, "setUdfpsOverlay: no fingerprint service");
+            return;
+        }
+
+        try {
+            mService.setUdfpsOverlay(controller);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 
     /**
      * Forwards BiometricStateListener to FingerprintService
diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
index 051e3a4..365a6b3 100644
--- a/core/java/android/hardware/fingerprint/IFingerprintService.aidl
+++ b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
@@ -26,6 +26,7 @@
 import android.hardware.fingerprint.IFingerprintServiceReceiver;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.hardware.fingerprint.ISidefpsController;
+import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.Fingerprint;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import java.util.List;
@@ -201,6 +202,10 @@
     @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     void setSidefpsController(in ISidefpsController controller);
 
+    // Sets the controller for managing the UDFPS overlay.
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+    void setUdfpsOverlay(in IUdfpsOverlay controller);
+
     // Registers BiometricStateListener.
     @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     void registerBiometricStateListener(IBiometricStateListener listener);
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt b/core/java/android/hardware/fingerprint/IUdfpsOverlay.aidl
similarity index 65%
copy from packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt
copy to core/java/android/hardware/fingerprint/IUdfpsOverlay.aidl
index 581dafa3..c99fccc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt
+++ b/core/java/android/hardware/fingerprint/IUdfpsOverlay.aidl
@@ -14,10 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.systemui.keyguard.domain.model
+package android.hardware.fingerprint;
 
-/** Enumerates all possible positions for quick affordances that can appear on the lock-screen. */
-enum class KeyguardQuickAffordancePosition {
-    BOTTOM_START,
-    BOTTOM_END,
+/**
+ * Interface for interacting with the under-display fingerprint sensor (UDFPS) overlay.
+ * @hide
+ */
+oneway interface IUdfpsOverlay {
+    // Shows the overlay.
+    void show(long requestId, int sensorId, int reason);
+
+    // Hides the overlay.
+    void hide(int sensorId);
 }
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index f213224b..49c0f92 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -161,4 +161,11 @@
     void registerBatteryListener(int deviceId, IInputDeviceBatteryListener listener);
 
     void unregisterBatteryListener(int deviceId, IInputDeviceBatteryListener listener);
+
+    // Get the bluetooth address of an input device if known, returning null if it either is not
+    // connected via bluetooth or if the address cannot be determined.
+    @EnforcePermission("BLUETOOTH")
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+            + "android.Manifest.permission.BLUETOOTH)")
+    String getInputDeviceBluetoothAddress(int deviceId);
 }
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 8d4aac4..0cf15f7 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -1481,6 +1481,24 @@
     }
 
     /**
+     * Returns the Bluetooth address of this input device, if known.
+     *
+     * The returned string is always null if this input device is not connected
+     * via Bluetooth, or if the Bluetooth address of the device cannot be
+     * determined. The returned address will look like: "11:22:33:44:55:66".
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.BLUETOOTH)
+    @Nullable
+    public String getInputDeviceBluetoothAddress(int deviceId) {
+        try {
+            return mIm.getInputDeviceBluetoothAddress(deviceId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Gets a vibrator service associated with an input device, always creates a new instance.
      * @return The vibrator, never null.
      * @hide
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index d3a6323..3c4abab 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -562,7 +562,7 @@
      *
      * @hide
      */
-    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
     public final native void markVintfStability();
 
     /**
@@ -1219,25 +1219,40 @@
     @UnsupportedAppUsage
     private boolean execTransact(int code, long dataObj, long replyObj,
             int flags) {
+
+        Parcel data = Parcel.obtain(dataObj);
+        Parcel reply = Parcel.obtain(replyObj);
+
         // At that point, the parcel request headers haven't been parsed so we do not know what
         // {@link WorkSource} the caller has set. Use calling UID as the default.
-        final int callingUid = Binder.getCallingUid();
-        final long origWorkSource = ThreadLocalWorkSource.setUid(callingUid);
+        //
+        // TODO: this is wrong - we should attribute along the entire call route
+        // also this attribution logic should move to native code - it only works
+        // for Java now
+        //
+        // This attribution support is not generic and therefore not support in RPC mode
+        final int callingUid = data.isForRpc() ? -1 : Binder.getCallingUid();
+        final long origWorkSource = callingUid == -1
+                ? -1 : ThreadLocalWorkSource.setUid(callingUid);
+
         try {
-            return execTransactInternal(code, dataObj, replyObj, flags, callingUid);
+            return execTransactInternal(code, data, reply, flags, callingUid);
         } finally {
-            ThreadLocalWorkSource.restore(origWorkSource);
+            reply.recycle();
+            data.recycle();
+
+            if (callingUid != -1) {
+                ThreadLocalWorkSource.restore(origWorkSource);
+            }
         }
     }
 
-    private boolean execTransactInternal(int code, long dataObj, long replyObj, int flags,
+    private boolean execTransactInternal(int code, Parcel data, Parcel reply, int flags,
             int callingUid) {
         // Make sure the observer won't change while processing a transaction.
         final BinderInternal.Observer observer = sObserver;
         final CallSession callSession =
                 observer != null ? observer.callStarted(this, code, UNSET_WORKSOURCE) : null;
-        Parcel data = Parcel.obtain(dataObj);
-        Parcel reply = Parcel.obtain(replyObj);
         // Theoretically, we should call transact, which will call onTransact,
         // but all that does is rewind it, and we just got these from an IPC,
         // so we'll just call it directly.
@@ -1268,8 +1283,10 @@
 
         final boolean tracingEnabled = tagEnabled && transactionTraceName != null;
         try {
+            // TODO - this logic should not be in Java - it should be in native
+            // code in libbinder so that it works for all binder users.
             final BinderCallHeavyHitterWatcher heavyHitterWatcher = sHeavyHitterWatcher;
-            if (heavyHitterWatcher != null) {
+            if (heavyHitterWatcher != null && callingUid != -1) {
                 // Notify the heavy hitter watcher, if it's enabled.
                 heavyHitterWatcher.onTransaction(callingUid, getClass(), code);
             }
@@ -1277,7 +1294,10 @@
                 Trace.traceBegin(Trace.TRACE_TAG_AIDL, transactionTraceName);
             }
 
-            if ((flags & FLAG_COLLECT_NOTED_APP_OPS) != 0) {
+            // TODO - this logic should not be in Java - it should be in native
+            // code in libbinder so that it works for all binder users. Further,
+            // this should not re-use flags.
+            if ((flags & FLAG_COLLECT_NOTED_APP_OPS) != 0 && callingUid != -1) {
                 AppOpsManager.startNotedAppOpsCollection(callingUid);
                 try {
                     res = onTransact(code, data, reply, flags);
@@ -1320,8 +1340,6 @@
             }
 
             checkParcel(this, code, reply, "Unreasonably large binder reply buffer");
-            reply.recycle();
-            data.recycle();
         }
 
         // Just in case -- we are done with the IPC, so there should be no more strict
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index d451765..2afa879 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -367,6 +367,8 @@
     @FastNative
     private static native void nativeMarkForBinder(long nativePtr, IBinder binder);
     @CriticalNative
+    private static native boolean nativeIsForRpc(long nativePtr);
+    @CriticalNative
     private static native int nativeDataSize(long nativePtr);
     @CriticalNative
     private static native int nativeDataAvail(long nativePtr);
@@ -644,6 +646,15 @@
         nativeMarkForBinder(mNativePtr, binder);
     }
 
+    /**
+     * Whether this Parcel is written for an RPC transaction.
+     *
+     * @hide
+     */
+    public final boolean isForRpc() {
+        return nativeIsForRpc(mNativePtr);
+    }
+
     /** @hide */
     @ParcelFlags
     @TestApi
diff --git a/core/java/android/os/PermissionEnforcer.java b/core/java/android/os/PermissionEnforcer.java
new file mode 100644
index 0000000..221e89a
--- /dev/null
+++ b/core/java/android/os/PermissionEnforcer.java
@@ -0,0 +1,101 @@
+/*
+ * 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.os;
+
+import android.annotation.NonNull;
+import android.annotation.SystemService;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.content.PermissionChecker;
+import android.permission.PermissionCheckerManager;
+
+/**
+ * PermissionEnforcer check permissions for AIDL-generated services which use
+ * the @EnforcePermission annotation.
+ *
+ * <p>AIDL services may be annotated with @EnforcePermission which will trigger
+ * the generation of permission check code. This generated code relies on
+ * PermissionEnforcer to validate the permissions. The methods available are
+ * purposely similar to the AIDL annotation syntax.
+ *
+ * @see android.permission.PermissionManager
+ *
+ * @hide
+ */
+@SystemService(Context.PERMISSION_ENFORCER_SERVICE)
+public class PermissionEnforcer {
+
+    private final Context mContext;
+
+    /** Protected constructor. Allows subclasses to instantiate an object
+     *  without using a Context.
+     */
+    protected PermissionEnforcer() {
+        mContext = null;
+    }
+
+    /** Constructor, prefer using the fromContext static method when possible */
+    public PermissionEnforcer(@NonNull Context context) {
+        mContext = context;
+    }
+
+    @PermissionCheckerManager.PermissionResult
+    protected int checkPermission(@NonNull String permission, @NonNull AttributionSource source) {
+        return PermissionChecker.checkPermissionForDataDelivery(
+            mContext, permission, PermissionChecker.PID_UNKNOWN, source, "" /* message */);
+    }
+
+    public void enforcePermission(@NonNull String permission, @NonNull
+            AttributionSource source) throws SecurityException {
+        int result = checkPermission(permission, source);
+        if (result != PermissionCheckerManager.PERMISSION_GRANTED) {
+            throw new SecurityException("Access denied, requires: " + permission);
+        }
+    }
+
+    public void enforcePermissionAllOf(@NonNull String[] permissions,
+            @NonNull AttributionSource source) throws SecurityException {
+        for (String permission : permissions) {
+            int result = checkPermission(permission, source);
+            if (result != PermissionCheckerManager.PERMISSION_GRANTED) {
+                throw new SecurityException("Access denied, requires: allOf={"
+                        + String.join(", ", permissions) + "}");
+            }
+        }
+    }
+
+    public void enforcePermissionAnyOf(@NonNull String[] permissions,
+            @NonNull AttributionSource source) throws SecurityException {
+        for (String permission : permissions) {
+            int result = checkPermission(permission, source);
+            if (result == PermissionCheckerManager.PERMISSION_GRANTED) {
+                return;
+            }
+        }
+        throw new SecurityException("Access denied, requires: anyOf={"
+                + String.join(", ", permissions) + "}");
+    }
+
+    /**
+     * Returns a new PermissionEnforcer based on a Context.
+     *
+     * @hide
+     */
+    public static PermissionEnforcer fromContext(@NonNull Context context) {
+        return context.getSystemService(PermissionEnforcer.class);
+    }
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index cd2bbeb..897b7c3 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -3374,9 +3374,26 @@
                     }
                 }
 
-                // Fetch all flags for the namespace at once for caching purposes
-                Bundle b = cp.call(cr.getAttributionSource(),
-                        mProviderHolder.mUri.getAuthority(), mCallListCommand, null, args);
+                Bundle b;
+                // b/252663068: if we're in system server and the caller did not call
+                // clearCallingIdentity, the read would fail due to mismatched AttributionSources.
+                // TODO(b/256013480): remove this bypass after fixing the callers in system server.
+                if (namespace.equals(DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER)
+                        && Settings.isInSystemServer()
+                        && Binder.getCallingUid() != Process.myUid()) {
+                    final long token = Binder.clearCallingIdentity();
+                    try {
+                        // Fetch all flags for the namespace at once for caching purposes
+                        b = cp.call(cr.getAttributionSource(),
+                                mProviderHolder.mUri.getAuthority(), mCallListCommand, null, args);
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                } else {
+                    // Fetch all flags for the namespace at once for caching purposes
+                    b = cp.call(cr.getAttributionSource(),
+                            mProviderHolder.mUri.getAuthority(), mCallListCommand, null, args);
+                }
                 if (b == null) {
                     // Invalid response, return an empty map
                     return keyValues;
@@ -7135,7 +7152,7 @@
          * Format like "ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0"
          * where imeId is ComponentName and subtype is int32.
          */
-        @Readable
+        @Readable(maxTargetSdk = Build.VERSION_CODES.TIRAMISU)
         public static final String ENABLED_INPUT_METHODS = "enabled_input_methods";
 
         /**
@@ -7144,7 +7161,7 @@
          * by ':'.
          * @hide
          */
-        @Readable
+        @Readable(maxTargetSdk = Build.VERSION_CODES.TIRAMISU)
         public static final String DISABLED_SYSTEM_INPUT_METHODS = "disabled_system_input_methods";
 
         /**
diff --git a/core/java/android/service/credentials/CredentialEntry.java b/core/java/android/service/credentials/CredentialEntry.java
index 4cc43a1..a3fa979 100644
--- a/core/java/android/service/credentials/CredentialEntry.java
+++ b/core/java/android/service/credentials/CredentialEntry.java
@@ -140,8 +140,8 @@
     public static final class Builder {
         private String mType;
         private Slice mSlice;
-        private PendingIntent mPendingIntent;
-        private Credential mCredential;
+        private PendingIntent mPendingIntent = null;
+        private Credential mCredential = null;
         private boolean mAutoSelectAllowed = false;
 
         /**
diff --git a/core/java/android/service/credentials/CredentialProviderException.java b/core/java/android/service/credentials/CredentialProviderException.java
index b39b4a0..06f0052 100644
--- a/core/java/android/service/credentials/CredentialProviderException.java
+++ b/core/java/android/service/credentials/CredentialProviderException.java
@@ -30,6 +30,22 @@
 public class CredentialProviderException extends Exception {
     public static final int ERROR_UNKNOWN = 0;
 
+    /**
+     * For internal use only.
+     * Error code to be used when the provider request times out.
+     *
+     * @hide
+     */
+    public static final int ERROR_TIMEOUT = 1;
+
+    /**
+     * For internal use only.
+     * Error code to be used when the async task is canceled internally.
+     *
+     * @hide
+     */
+    public static final int ERROR_TASK_CANCELED = 2;
+
     private final int mErrorCode;
 
     /**
@@ -37,6 +53,8 @@
      */
     @IntDef(prefix = {"ERROR_"}, value = {
             ERROR_UNKNOWN,
+            ERROR_TIMEOUT,
+            ERROR_TASK_CANCELED
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CredentialProviderError { }
diff --git a/core/java/android/service/credentials/CredentialProviderInfo.java b/core/java/android/service/credentials/CredentialProviderInfo.java
index e3f8cb7..2c7a983 100644
--- a/core/java/android/service/credentials/CredentialProviderInfo.java
+++ b/core/java/android/service/credentials/CredentialProviderInfo.java
@@ -18,16 +18,21 @@
 
 import android.Manifest;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.app.AppGlobals;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageItemInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
 import android.os.RemoteException;
+import android.text.TextUtils;
 import android.util.Log;
 import android.util.Slog;
 
@@ -48,19 +53,21 @@
     @NonNull
     private final List<String> mCapabilities;
 
-    // TODO: Move the two strings below to CredentialProviderService when ready.
-    private static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities";
-    private static final String SERVICE_INTERFACE =
-            "android.service.credentials.CredentialProviderService";
-
+    @NonNull
+    private final Context mContext;
+    @Nullable
+    private final Drawable mIcon;
+    @Nullable
+    private final CharSequence mLabel;
 
     /**
      * Constructs an information instance of the credential provider.
      *
-     * @param context The context object
-     * @param serviceComponent The serviceComponent of the provider service
-     * @param userId The android userId for which the current process is running
+     * @param context the context object
+     * @param serviceComponent the serviceComponent of the provider service
+     * @param userId the android userId for which the current process is running
      * @throws PackageManager.NameNotFoundException If provider service is not found
+     * @throws SecurityException If provider does not require the relevant permission
      */
     public CredentialProviderInfo(@NonNull Context context,
             @NonNull ComponentName serviceComponent, int userId)
@@ -68,7 +75,13 @@
         this(context, getServiceInfoOrThrow(serviceComponent, userId));
     }
 
-    private CredentialProviderInfo(@NonNull Context context, @NonNull ServiceInfo serviceInfo) {
+    /**
+     * Constructs an information instance of the credential provider.
+     * @param context the context object
+     * @param serviceInfo the service info for the provider app. This must be retrieved from the
+     *                    {@code PackageManager}
+     */
+    public CredentialProviderInfo(@NonNull Context context, @NonNull ServiceInfo serviceInfo) {
         if (!Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE.equals(serviceInfo.permission)) {
             Log.i(TAG, "Credential Provider Service from : " + serviceInfo.packageName
                     + "does not require permission"
@@ -76,32 +89,43 @@
             throw new SecurityException("Service does not require the expected permission : "
                     + Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE);
         }
+        mContext = context;
         mServiceInfo = serviceInfo;
         mCapabilities = new ArrayList<>();
-        populateProviderCapabilities(context);
+        mIcon = mServiceInfo.loadIcon(mContext.getPackageManager());
+        mLabel = mServiceInfo.loadSafeLabel(
+                mContext.getPackageManager(), 0 /* do not ellipsize */,
+                TextUtils.SAFE_STRING_FLAG_FIRST_LINE | TextUtils.SAFE_STRING_FLAG_TRIM);
+        populateProviderCapabilities(context, serviceInfo);
     }
 
-    private void populateProviderCapabilities(@NonNull Context context) {
-        if (mServiceInfo.applicationInfo.metaData == null) {
-            return;
-        }
+    private void populateProviderCapabilities(@NonNull Context context, ServiceInfo serviceInfo) {
+        final PackageManager pm = context.getPackageManager();
         try {
-            final int resourceId = mServiceInfo.applicationInfo.metaData.getInt(
-                    CAPABILITY_META_DATA_KEY);
-            String[] capabilities = context.getResources().getStringArray(resourceId);
-            if (capabilities == null) {
-                Log.w(TAG, "No capabilities found for provider: " + mServiceInfo.packageName);
+            Bundle metadata = serviceInfo.metaData;
+            Resources resources = pm.getResourcesForApplication(serviceInfo.applicationInfo);
+            if (metadata == null || resources == null) {
+                Log.i(TAG, "populateProviderCapabilities - metadata or resources is null");
                 return;
             }
+
+            String[] capabilities = resources.getStringArray(metadata.getInt(
+                    CredentialProviderService.CAPABILITY_META_DATA_KEY));
+            if (capabilities == null || capabilities.length == 0) {
+                Slog.i(TAG, "No capabilities found for provider:" + serviceInfo.packageName);
+                return;
+            }
+
             for (String capability : capabilities) {
                 if (capability.isEmpty()) {
-                    Log.w(TAG, "Skipping empty capability");
+                    Slog.i(TAG, "Skipping empty capability");
                     continue;
                 }
+                Slog.i(TAG, "Capabilities found for provider: " + capability);
                 mCapabilities.add(capability);
             }
-        } catch (Resources.NotFoundException e) {
-            Log.w(TAG, "Exception while populating provider capabilities: " + e.getMessage());
+        } catch (PackageManager.NameNotFoundException e) {
+            Slog.i(TAG, e.getMessage());
         }
     }
 
@@ -135,6 +159,18 @@
         return mServiceInfo;
     }
 
+    /** Returns the service icon. */
+    @Nullable
+    public Drawable getServiceIcon() {
+        return mIcon;
+    }
+
+    /** Returns the service label. */
+    @Nullable
+    public CharSequence getServiceLabel() {
+        return mLabel;
+    }
+
     /** Returns an immutable list of capabilities this provider service can support. */
     @NonNull
     public List<String> getCapabilities() {
@@ -145,14 +181,15 @@
      * Returns the valid credential provider services available for the user with the
      * given {@code userId}.
      */
+    @NonNull
     public static List<CredentialProviderInfo> getAvailableServices(@NonNull Context context,
             @UserIdInt int userId) {
         final List<CredentialProviderInfo> services = new ArrayList<>();
 
         final List<ResolveInfo> resolveInfos =
                 context.getPackageManager().queryIntentServicesAsUser(
-                        new Intent(SERVICE_INTERFACE),
-                        PackageManager.GET_META_DATA,
+                        new Intent(CredentialProviderService.SERVICE_INTERFACE),
+                        PackageManager.ResolveInfoFlags.of(PackageManager.GET_META_DATA),
                         userId);
         for (ResolveInfo resolveInfo : resolveInfos) {
             final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
@@ -169,8 +206,9 @@
      * Returns the valid credential provider services available for the user, that can
      * support the given {@code credentialType}.
      */
+    @NonNull
     public static List<CredentialProviderInfo> getAvailableServicesForCapability(
-            Context context, @UserIdInt int userId, String credentialType) {
+            @NonNull Context context, @UserIdInt int userId, @NonNull String credentialType) {
         List<CredentialProviderInfo> servicesForCapability = new ArrayList<>();
         final List<CredentialProviderInfo> services = getAvailableServices(context, userId);
 
diff --git a/core/java/android/service/credentials/CredentialProviderService.java b/core/java/android/service/credentials/CredentialProviderService.java
index 1cdf186..b1b08f4 100644
--- a/core/java/android/service/credentials/CredentialProviderService.java
+++ b/core/java/android/service/credentials/CredentialProviderService.java
@@ -42,6 +42,9 @@
  */
 public abstract class CredentialProviderService extends Service {
     private static final String TAG = "CredProviderService";
+
+    public static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities";
+
     private Handler mHandler;
 
     /**
@@ -71,12 +74,13 @@
 
     private final ICredentialProviderService mInterface = new ICredentialProviderService.Stub() {
         @Override
-        public void onGetCredentials(GetCredentialsRequest request, ICancellationSignal transport,
+        public ICancellationSignal onGetCredentials(GetCredentialsRequest request,
                 IGetCredentialsCallback callback) {
             Objects.requireNonNull(request);
-            Objects.requireNonNull(transport);
             Objects.requireNonNull(callback);
 
+            ICancellationSignal transport = CancellationSignal.createTransport();
+
             mHandler.sendMessage(obtainMessage(
                     CredentialProviderService::onGetCredentials,
                     CredentialProviderService.this, request,
@@ -100,15 +104,17 @@
                         }
                     }
             ));
+            return transport;
         }
 
         @Override
-        public void onCreateCredential(CreateCredentialRequest request,
-                ICancellationSignal transport, ICreateCredentialCallback callback) {
+        public ICancellationSignal onCreateCredential(CreateCredentialRequest request,
+                ICreateCredentialCallback callback) {
             Objects.requireNonNull(request);
-            Objects.requireNonNull(transport);
             Objects.requireNonNull(callback);
 
+            ICancellationSignal transport = CancellationSignal.createTransport();
+
             mHandler.sendMessage(obtainMessage(
                     CredentialProviderService::onCreateCredential,
                     CredentialProviderService.this, request,
@@ -132,6 +138,7 @@
                         }
                     }
             ));
+            return transport;
         }
     };
 
diff --git a/core/java/android/service/credentials/ICredentialProviderService.aidl b/core/java/android/service/credentials/ICredentialProviderService.aidl
index c68430c..c21cefa 100644
--- a/core/java/android/service/credentials/ICredentialProviderService.aidl
+++ b/core/java/android/service/credentials/ICredentialProviderService.aidl
@@ -21,13 +21,14 @@
 import android.service.credentials.CreateCredentialRequest;
 import android.service.credentials.IGetCredentialsCallback;
 import android.service.credentials.ICreateCredentialCallback;
+import android.os.ICancellationSignal;
 
 /**
  * Interface from the system to a credential provider service.
  *
  * @hide
  */
-oneway interface ICredentialProviderService {
-    void onGetCredentials(in GetCredentialsRequest request, in ICancellationSignal transport, in IGetCredentialsCallback callback);
-    void onCreateCredential(in CreateCredentialRequest request, in ICancellationSignal transport, in ICreateCredentialCallback callback);
+interface ICredentialProviderService {
+    ICancellationSignal onGetCredentials(in GetCredentialsRequest request, in IGetCredentialsCallback callback);
+    ICancellationSignal onCreateCredential(in CreateCredentialRequest request, in ICreateCredentialCallback callback);
 }
diff --git a/core/java/android/service/dreams/DreamManagerInternal.java b/core/java/android/service/dreams/DreamManagerInternal.java
index 295171c..5f30ad0 100644
--- a/core/java/android/service/dreams/DreamManagerInternal.java
+++ b/core/java/android/service/dreams/DreamManagerInternal.java
@@ -54,6 +54,13 @@
     public abstract void requestDream();
 
     /**
+     * Whether dreaming can start given user settings and the current dock/charge state.
+     *
+     * @param isScreenOn True if the screen is currently on.
+     */
+    public abstract boolean canStartDreaming(boolean isScreenOn);
+
+    /**
      * Called by the ActivityTaskManagerService to verify that the startDreamActivity
      * request comes from the current active dream component.
      *
diff --git a/core/java/android/text/method/BaseKeyListener.java b/core/java/android/text/method/BaseKeyListener.java
index d4bcd12..01989d5 100644
--- a/core/java/android/text/method/BaseKeyListener.java
+++ b/core/java/android/text/method/BaseKeyListener.java
@@ -229,6 +229,8 @@
                         break;
                     } else if (Emoji.isEmojiModifierBase(codePoint)) {
                         deleteCharCount += Character.charCount(codePoint);
+                        state = STATE_BEFORE_EMOJI;
+                        break;
                     }
                     state = STATE_FINISHED;
                     break;
diff --git a/core/java/android/util/DisplayMetrics.java b/core/java/android/util/DisplayMetrics.java
index 0a3e6b1..517d982 100755
--- a/core/java/android/util/DisplayMetrics.java
+++ b/core/java/android/util/DisplayMetrics.java
@@ -174,6 +174,14 @@
      * This is not a density that applications should target, instead relying
      * on the system to scale their {@link #DENSITY_XXXHIGH} assets for them.
      */
+    public static final int DENSITY_520 = 520;
+
+    /**
+     * Intermediate density for screens that sit somewhere between
+     * {@link #DENSITY_XXHIGH} (480 dpi) and {@link #DENSITY_XXXHIGH} (640 dpi).
+     * This is not a density that applications should target, instead relying
+     * on the system to scale their {@link #DENSITY_XXXHIGH} assets for them.
+     */
     public static final int DENSITY_560 = 560;
 
     /**
diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java
index 9b1d867..799955b 100644
--- a/core/java/android/view/InputDevice.java
+++ b/core/java/android/view/InputDevice.java
@@ -16,6 +16,7 @@
 
 package android.view;
 
+import android.Manifest;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -1010,6 +1011,22 @@
     }
 
     /**
+     * Returns the Bluetooth address of this input device, if known.
+     *
+     * The returned string is always null if this input device is not connected
+     * via Bluetooth, or if the Bluetooth address of the device cannot be
+     * determined. The returned address will look like: "11:22:33:44:55:66".
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.BLUETOOTH)
+    @Nullable
+    public String getBluetoothAddress() {
+        // We query the address via a separate InputManager API instead of pre-populating it in
+        // this class to avoid leaking it to apps that do not have sufficient permissions.
+        return InputManager.getInstance().getInputDeviceBluetoothAddress(mId);
+    }
+
+    /**
      * Gets the vibrator service associated with the device, if there is one.
      * Even if the device does not have a vibrator, the result is never null.
      * Use {@link Vibrator#hasVibrator} to determine whether a vibrator is
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 5e836ef..ff4588a 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -4970,7 +4970,7 @@
     }
 
     void reportKeepClearAreasChanged() {
-        if (!mHasPendingKeepClearAreaChange) {
+        if (!mHasPendingKeepClearAreaChange || mView == null) {
             return;
         }
         mHasPendingKeepClearAreaChange = false;
diff --git a/core/java/com/android/internal/security/TEST_MAPPING b/core/java/com/android/internal/security/TEST_MAPPING
index 9a5e90e..803760c 100644
--- a/core/java/com/android/internal/security/TEST_MAPPING
+++ b/core/java/com/android/internal/security/TEST_MAPPING
@@ -1,6 +1,17 @@
 {
   "presubmit": [
     {
+      "name": "FrameworksCoreTests",
+      "options": [
+        {
+          "include-filter": "com.android.internal.security."
+        },
+        {
+          "include-annotation": "android.platform.test.annotations.Presubmit"
+        }
+      ]
+    },
+    {
       "name": "ApkVerityTest",
       "file_patterns": ["VerityUtils\\.java"]
     }
diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java
index cb5820f..7f45c09 100644
--- a/core/java/com/android/internal/security/VerityUtils.java
+++ b/core/java/com/android/internal/security/VerityUtils.java
@@ -23,10 +23,28 @@
 import android.system.OsConstants;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
+import com.android.internal.org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import com.android.internal.org.bouncycastle.cms.CMSException;
+import com.android.internal.org.bouncycastle.cms.CMSProcessableByteArray;
+import com.android.internal.org.bouncycastle.cms.CMSSignedData;
+import com.android.internal.org.bouncycastle.cms.SignerInformation;
+import com.android.internal.org.bouncycastle.cms.SignerInformationVerifier;
+import com.android.internal.org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
+import com.android.internal.org.bouncycastle.operator.OperatorCreationException;
+
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Paths;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
 
 /** Provides fsverity related operations. */
 public abstract class VerityUtils {
@@ -91,6 +109,91 @@
     }
 
     /**
+     * Verifies the signature over the fs-verity digest using the provided certificate.
+     *
+     * This method should only be used by any existing fs-verity use cases that require
+     * PKCS#7 signature verification, if backward compatibility is necessary.
+     *
+     * Since PKCS#7 is too flexible, for the current specific need, only specific configuration
+     * will be accepted:
+     * <ul>
+     *   <li>Must use SHA256 as the digest algorithm
+     *   <li>Must use rsaEncryption as signature algorithm
+     *   <li>Must be detached / without content
+     *   <li>Must not include any signed or unsigned attributes
+     * </ul>
+     *
+     * It is up to the caller to provide an appropriate/trusted certificate.
+     *
+     * @param signatureBlock byte array of a PKCS#7 detached signature
+     * @param digest fs-verity digest with the common configuration using sha256
+     * @param derCertInputStream an input stream of a X.509 certificate in DER
+     * @return whether the verification succeeds
+     */
+    public static boolean verifyPkcs7DetachedSignature(@NonNull byte[] signatureBlock,
+            @NonNull byte[] digest, @NonNull InputStream derCertInputStream) {
+        if (digest.length != 32) {
+            Slog.w(TAG, "Only sha256 is currently supported");
+            return false;
+        }
+
+        try {
+            CMSSignedData signedData = new CMSSignedData(
+                    new CMSProcessableByteArray(toFormattedDigest(digest)),
+                    signatureBlock);
+
+            if (!signedData.isDetachedSignature()) {
+                Slog.w(TAG, "Expect only detached siganture");
+                return false;
+            }
+            if (!signedData.getCertificates().getMatches(null).isEmpty()) {
+                Slog.w(TAG, "Expect no certificate in signature");
+                return false;
+            }
+            if (!signedData.getCRLs().getMatches(null).isEmpty()) {
+                Slog.w(TAG, "Expect no CRL in signature");
+                return false;
+            }
+
+            X509Certificate trustedCert = (X509Certificate) CertificateFactory.getInstance("X.509")
+                    .generateCertificate(derCertInputStream);
+            SignerInformationVerifier verifier = new JcaSimpleSignerInfoVerifierBuilder()
+                    .build(trustedCert);
+
+            // Verify any signature with the trusted certificate.
+            for (SignerInformation si : signedData.getSignerInfos().getSigners()) {
+                // To be the most strict while dealing with the complicated PKCS#7 signature, reject
+                // everything we don't need.
+                if (si.getSignedAttributes() != null && si.getSignedAttributes().size() > 0) {
+                    Slog.w(TAG, "Unexpected signed attributes");
+                    return false;
+                }
+                if (si.getUnsignedAttributes() != null && si.getUnsignedAttributes().size() > 0) {
+                    Slog.w(TAG, "Unexpected unsigned attributes");
+                    return false;
+                }
+                if (!NISTObjectIdentifiers.id_sha256.getId().equals(si.getDigestAlgOID())) {
+                    Slog.w(TAG, "Unsupported digest algorithm OID: " + si.getDigestAlgOID());
+                    return false;
+                }
+                if (!PKCSObjectIdentifiers.rsaEncryption.getId().equals(si.getEncryptionAlgOID())) {
+                    Slog.w(TAG, "Unsupported encryption algorithm OID: "
+                            + si.getEncryptionAlgOID());
+                    return false;
+                }
+
+                if (si.verify(verifier)) {
+                    return true;
+                }
+            }
+            return false;
+        } catch (CertificateException | CMSException | OperatorCreationException e) {
+            Slog.w(TAG, "Error occurred during the PKCS#7 signature verification", e);
+        }
+        return false;
+    }
+
+    /**
      * Returns fs-verity digest for the file if enabled, otherwise returns null. The digest is a
      * hash of root hash of fs-verity's Merkle tree with extra metadata.
      *
@@ -110,6 +213,19 @@
         return result;
     }
 
+    /** @hide */
+    @VisibleForTesting
+    public static byte[] toFormattedDigest(byte[] digest) {
+        // Construct fsverity_formatted_digest used in fs-verity's built-in signature verification.
+        ByteBuffer buffer = ByteBuffer.allocate(12 + digest.length); // struct size + sha256 size
+        buffer.order(ByteOrder.LITTLE_ENDIAN);
+        buffer.put("FSVerity".getBytes(StandardCharsets.US_ASCII));
+        buffer.putShort((short) 1); // FS_VERITY_HASH_ALG_SHA256
+        buffer.putShort((short) digest.length);
+        buffer.put(digest);
+        return buffer.array();
+    }
+
     private static native int enableFsverityNative(@NonNull String filePath,
             @NonNull byte[] pkcs7Signature);
     private static native int measureFsverityNative(@NonNull String filePath,
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index d59a51a..c50abb3 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -329,7 +329,6 @@
             header_libs: [
                 "bionic_libc_platform_headers",
                 "dnsproxyd_protocol_headers",
-                "libandroid_runtime_vm_headers",
             ],
         },
         host: {
@@ -418,24 +417,3 @@
         never: true,
     },
 }
-
-cc_library_headers {
-    name: "libandroid_runtime_vm_headers",
-    host_supported: true,
-    vendor_available: true,
-    // TODO(b/153609531): remove when libbinder is not native_bridge_supported
-    native_bridge_supported: true,
-    // Allow only modules from the following list to create threads that can be
-    // attached to the JVM. This list should be a subset of the dependencies of
-    // libandroid_runtime.
-    visibility: [
-        "//frameworks/native/libs/binder",
-    ],
-    export_include_dirs: ["include_vm"],
-    header_libs: [
-        "jni_headers",
-    ],
-    export_header_lib_headers: [
-        "jni_headers",
-    ],
-}
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 422bdc9..9da28a3 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -22,7 +22,6 @@
 #include <android-base/properties.h>
 #include <android/graphics/jni_runtime.h>
 #include <android_runtime/AndroidRuntime.h>
-#include <android_runtime/vm.h>
 #include <assert.h>
 #include <binder/IBinder.h>
 #include <binder/IPCThreadState.h>
diff --git a/core/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp
index 1f64df4..4d8dac1 100644
--- a/core/jni/android_os_Parcel.cpp
+++ b/core/jni/android_os_Parcel.cpp
@@ -116,6 +116,11 @@
     }
 }
 
+static jboolean android_os_Parcel_isForRpc(jlong nativePtr) {
+    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
+    return parcel ? parcel->isForRpc() : false;
+}
+
 static jint android_os_Parcel_dataSize(jlong nativePtr)
 {
     Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
@@ -808,6 +813,8 @@
     // @FastNative
     {"nativeMarkForBinder",       "(JLandroid/os/IBinder;)V", (void*)android_os_Parcel_markForBinder},
     // @CriticalNative
+    {"nativeIsForRpc",            "(J)Z", (void*)android_os_Parcel_isForRpc},
+    // @CriticalNative
     {"nativeDataSize",            "(J)I", (void*)android_os_Parcel_dataSize},
     // @CriticalNative
     {"nativeDataAvail",           "(J)I", (void*)android_os_Parcel_dataAvail},
diff --git a/core/jni/android_view_InputDevice.cpp b/core/jni/android_view_InputDevice.cpp
index 39ec037..b2994f4 100644
--- a/core/jni/android_view_InputDevice.cpp
+++ b/core/jni/android_view_InputDevice.cpp
@@ -70,6 +70,8 @@
                                           deviceInfo.hasMic(), deviceInfo.hasButtonUnderPad(),
                                           deviceInfo.hasSensor(), deviceInfo.hasBattery(),
                                           deviceInfo.supportsUsi()));
+    // Note: We do not populate the Bluetooth address into the InputDevice object to avoid leaking
+    // it to apps that do not have the Bluetooth permission.
 
     const std::vector<InputDeviceInfo::MotionRange>& ranges = deviceInfo.getMotionRanges();
     for (const InputDeviceInfo::MotionRange& range: ranges) {
diff --git a/core/jni/include_vm/android_runtime/vm.h b/core/jni/include_vm/android_runtime/vm.h
deleted file mode 100644
index a6e7c16..0000000
--- a/core/jni/include_vm/android_runtime/vm.h
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2021 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.
- */
-
-#pragma once
-
-#include <jni.h>
-
-// Get the Java VM. If the symbol doesn't exist at runtime, it means libandroid_runtime
-// is not loaded in the current process. If the symbol exists but it returns nullptr, it
-// means JavaVM is not yet started.
-extern "C" JavaVM* AndroidRuntimeGetJavaVM();
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 554b153..62c5848 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -6622,6 +6622,15 @@
     <permission android:name="android.permission.MANAGE_DEVICE_LOCK_STATE"
                 android:protectionLevel="internal|role" />
 
+    <!-- Allows applications to use the long running jobs APIs.
+         <p>This is a special access permission that can be revoked by the system or the user.
+         <p>Apps need to target API {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or above
+         to be able to request this permission.
+         <p>Protection level: appop
+     -->
+    <permission android:name="android.permission.RUN_LONG_JOBS"
+                android:protectionLevel="normal|appop"/>
+
     <!-- Attribution for Geofencing service. -->
     <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
     <!-- Attribution for Country Detector. -->
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java
new file mode 100644
index 0000000..fe3ab62
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java
@@ -0,0 +1,433 @@
+/*
+ * 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 static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.hardware.radio.IRadioService;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioMetadata;
+import android.hardware.radio.RadioTuner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class TunerAdapterTest {
+
+    private static final int CALLBACK_TIMEOUT_MS = 30_000;
+    private static final int AM_LOWER_LIMIT_KHZ = 150;
+
+    private static final RadioManager.BandConfig TEST_BAND_CONFIG = createBandConfig();
+
+    private static final ProgramSelector.Identifier FM_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY,
+                    /* value= */ 94300);
+    private static final ProgramSelector FM_SELECTOR =
+            new ProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, FM_IDENTIFIER,
+                    /* secondaryIds= */ null, /* vendorIds= */ null);
+    private static final RadioManager.ProgramInfo FM_PROGRAM_INFO = createFmProgramInfo();
+
+    private RadioTuner mRadioTuner;
+    private ITunerCallback mTunerCallback;
+
+    @Mock
+    private IRadioService mRadioServiceMock;
+    @Mock
+    private Context mContextMock;
+    @Mock
+    private ITuner mTunerMock;
+    @Mock
+    private RadioTuner.Callback mCallbackMock;
+
+    @Before
+    public void setUp() throws Exception {
+        RadioManager radioManager = new RadioManager(mContextMock, mRadioServiceMock);
+
+        doAnswer(invocation -> {
+            mTunerCallback = (ITunerCallback) invocation.getArguments()[3];
+            return mTunerMock;
+        }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any());
+
+        doAnswer(invocation -> {
+            ProgramSelector program = (ProgramSelector) invocation.getArguments()[0];
+            if (program.getPrimaryId().getType()
+                    != ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY) {
+                throw new IllegalArgumentException();
+            }
+            if (program.getPrimaryId().getValue() < AM_LOWER_LIMIT_KHZ) {
+                mTunerCallback.onTuneFailed(RadioManager.STATUS_BAD_VALUE, program);
+            } else {
+                mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO);
+            }
+            return RadioManager.STATUS_OK;
+        }).when(mTunerMock).tune(any());
+
+        mRadioTuner = radioManager.openTuner(/* moduleId= */ 0, TEST_BAND_CONFIG,
+                /* withAudio= */ true, mCallbackMock, /* handler= */ null);
+    }
+
+    @After
+    public void cleanUp() throws Exception {
+        mRadioTuner.close();
+    }
+
+    @Test
+    public void close_forTunerAdapter() throws Exception {
+        mRadioTuner.close();
+
+        verify(mTunerMock).close();
+    }
+
+    @Test
+    public void setConfiguration_forTunerAdapter() throws Exception {
+        int status = mRadioTuner.setConfiguration(TEST_BAND_CONFIG);
+
+        verify(mTunerMock).setConfiguration(TEST_BAND_CONFIG);
+        assertWithMessage("Status for setting configuration")
+                .that(status).isEqualTo(RadioManager.STATUS_OK);
+    }
+
+    @Test
+    public void getConfiguration_forTunerAdapter() throws Exception {
+        when(mTunerMock.getConfiguration()).thenReturn(TEST_BAND_CONFIG);
+        RadioManager.BandConfig[] bandConfigs = new RadioManager.BandConfig[1];
+
+        int status = mRadioTuner.getConfiguration(bandConfigs);
+
+        assertWithMessage("Status for getting configuration")
+                .that(status).isEqualTo(RadioManager.STATUS_OK);
+        assertWithMessage("Configuration obtained from radio tuner")
+                .that(bandConfigs[0]).isEqualTo(TEST_BAND_CONFIG);
+    }
+
+    @Test
+    public void setMute_forTunerAdapter() {
+        int status = mRadioTuner.setMute(/* mute= */ true);
+
+        assertWithMessage("Status for setting mute")
+                .that(status).isEqualTo(RadioManager.STATUS_OK);
+    }
+
+    @Test
+    public void getMute_forTunerAdapter() throws Exception {
+        when(mTunerMock.isMuted()).thenReturn(true);
+
+        boolean muteStatus = mRadioTuner.getMute();
+
+        assertWithMessage("Mute status").that(muteStatus).isTrue();
+    }
+
+    @Test
+    public void step_forTunerAdapter_succeeds() throws Exception {
+        doAnswer(invocation -> {
+            mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO);
+            return RadioManager.STATUS_OK;
+        }).when(mTunerMock).step(anyBoolean(), anyBoolean());
+
+        int scanStatus = mRadioTuner.step(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ false);
+
+        verify(mTunerMock).step(/* skipSubChannel= */ true, /* skipSubChannel= */ false);
+        assertWithMessage("Status for stepping")
+                .that(scanStatus).isEqualTo(RadioManager.STATUS_OK);
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO);
+    }
+
+    @Test
+    public void seek_forTunerAdapter_succeeds() throws Exception {
+        doAnswer(invocation -> {
+            mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO);
+            return RadioManager.STATUS_OK;
+        }).when(mTunerMock).scan(anyBoolean(), anyBoolean());
+
+        int scanStatus = mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ false);
+
+        verify(mTunerMock).scan(/* directionDown= */ true, /* skipSubChannel= */ false);
+        assertWithMessage("Status for seeking")
+                .that(scanStatus).isEqualTo(RadioManager.STATUS_OK);
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO);
+    }
+
+    @Test
+    public void seek_forTunerAdapter_invokesOnErrorWhenTimeout() throws Exception {
+        doAnswer(invocation -> {
+            mTunerCallback.onError(RadioTuner.ERROR_SCAN_TIMEOUT);
+            return RadioManager.STATUS_OK;
+        }).when(mTunerMock).scan(anyBoolean(), anyBoolean());
+
+        mRadioTuner.scan(RadioTuner.DIRECTION_UP, /* skipSubChannel*/ true);
+
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onError(RadioTuner.ERROR_SCAN_TIMEOUT);
+    }
+
+    @Test
+    public void tune_withChannelsForTunerAdapter_succeeds() {
+        int status = mRadioTuner.tune(/* channel= */ 92300, /* subChannel= */ 0);
+
+        assertWithMessage("Status for tuning with channel and sub-channel")
+                .that(status).isEqualTo(RadioManager.STATUS_OK);
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO);
+    }
+
+    @Test
+    public void tune_withValidSelectorForTunerAdapter_succeeds() throws Exception {
+        mRadioTuner.tune(FM_SELECTOR);
+
+        verify(mTunerMock).tune(FM_SELECTOR);
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO);
+    }
+
+
+    @Test
+    public void tune_withInvalidSelectorForTunerAdapter_invokesOnTuneFailed() {
+        ProgramSelector invalidSelector = new ProgramSelector(ProgramSelector.PROGRAM_TYPE_FM,
+                        new ProgramSelector.Identifier(
+                                ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 100),
+                /* secondaryIds= */ null, /* vendorIds= */ null);
+
+        mRadioTuner.tune(invalidSelector);
+
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS))
+                .onTuneFailed(RadioManager.STATUS_BAD_VALUE, invalidSelector);
+    }
+
+    @Test
+    public void cancel_forTunerAdapter() throws Exception {
+        mRadioTuner.tune(FM_SELECTOR);
+
+        mRadioTuner.cancel();
+
+        verify(mTunerMock).cancel();
+    }
+
+    @Test
+    public void cancelAnnouncement_forTunerAdapter() throws Exception {
+        mRadioTuner.cancelAnnouncement();
+
+        verify(mTunerMock).cancelAnnouncement();
+    }
+
+    @Test
+    public void getProgramInfo_beforeProgramInfoSetForTunerAdapter() {
+        RadioManager.ProgramInfo[] programInfoArray = new RadioManager.ProgramInfo[1];
+
+        int status = mRadioTuner.getProgramInformation(programInfoArray);
+
+        assertWithMessage("Status for getting null program info")
+                .that(status).isEqualTo(RadioManager.STATUS_INVALID_OPERATION);
+    }
+
+    @Test
+    public void getProgramInfo_afterTuneForTunerAdapter() {
+        mRadioTuner.tune(FM_SELECTOR);
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO);
+        RadioManager.ProgramInfo[] programInfoArray = new RadioManager.ProgramInfo[1];
+
+        int status = mRadioTuner.getProgramInformation(programInfoArray);
+
+        assertWithMessage("Status for getting program info")
+                .that(status).isEqualTo(RadioManager.STATUS_OK);
+        assertWithMessage("Program info obtained from radio tuner")
+                .that(programInfoArray[0]).isEqualTo(FM_PROGRAM_INFO);
+    }
+
+    @Test
+    public void getMetadataImage_forTunerAdapter() throws Exception {
+        Bitmap bitmapExpected = Mockito.mock(Bitmap.class);
+        when(mTunerMock.getImage(anyInt())).thenReturn(bitmapExpected);
+        int imageId = 1;
+
+        Bitmap image = mRadioTuner.getMetadataImage(/* id= */ imageId);
+
+        assertWithMessage("Image obtained from id %s", imageId)
+                .that(image).isEqualTo(bitmapExpected);
+    }
+
+    @Test
+    public void isAnalogForced_forTunerAdapter() throws Exception {
+        when(mTunerMock.isConfigFlagSet(RadioManager.CONFIG_FORCE_ANALOG)).thenReturn(true);
+
+        boolean isAnalogForced = mRadioTuner.isAnalogForced();
+
+        assertWithMessage("Forced analog playback switch")
+                .that(isAnalogForced).isTrue();
+    }
+
+    @Test
+    public void setAnalogForced_forTunerAdapter() throws Exception {
+        boolean analogForced = true;
+
+        mRadioTuner.setAnalogForced(analogForced);
+
+        verify(mTunerMock).setConfigFlag(RadioManager.CONFIG_FORCE_ANALOG, analogForced);
+    }
+
+    @Test
+    public void isConfigFlagSupported_forTunerAdapter() throws Exception {
+        when(mTunerMock.isConfigFlagSupported(RadioManager.CONFIG_DAB_DAB_LINKING))
+                .thenReturn(true);
+
+        boolean dabFmSoftLinking =
+                mRadioTuner.isConfigFlagSupported(RadioManager.CONFIG_DAB_DAB_LINKING);
+
+        assertWithMessage("Support for DAB-DAB linking config flag")
+                .that(dabFmSoftLinking).isTrue();
+    }
+
+    @Test
+    public void isConfigFlagSet_forTunerAdapter() throws Exception {
+        when(mTunerMock.isConfigFlagSet(RadioManager.CONFIG_DAB_FM_SOFT_LINKING))
+                .thenReturn(true);
+
+        boolean dabFmSoftLinking =
+                mRadioTuner.isConfigFlagSet(RadioManager.CONFIG_DAB_FM_SOFT_LINKING);
+
+        assertWithMessage("DAB-FM soft linking config flag")
+                .that(dabFmSoftLinking).isTrue();
+    }
+
+    @Test
+    public void setConfigFlag_forTunerAdapter() throws Exception {
+        boolean dabFmLinking = true;
+
+        mRadioTuner.setConfigFlag(RadioManager.CONFIG_DAB_FM_LINKING, dabFmLinking);
+
+        verify(mTunerMock).setConfigFlag(RadioManager.CONFIG_DAB_FM_LINKING, dabFmLinking);
+    }
+
+    @Test
+    public void getParameters_forTunerAdapter() throws Exception {
+        List<String> parameterKeys = Arrays.asList("ParameterKeyMock");
+        Map<String, String> parameters = Map.of("ParameterKeyMock", "ParameterValueMock");
+        when(mTunerMock.getParameters(parameterKeys)).thenReturn(parameters);
+
+        assertWithMessage("Parameters obtained from radio tuner")
+                .that(mRadioTuner.getParameters(parameterKeys)).isEqualTo(parameters);
+    }
+
+    @Test
+    public void setParameters_forTunerAdapter() throws Exception {
+        Map<String, String> parameters = Map.of("ParameterKeyMock", "ParameterValueMock");
+        when(mTunerMock.setParameters(parameters)).thenReturn(parameters);
+
+        assertWithMessage("Parameters set for radio tuner")
+                .that(mRadioTuner.setParameters(parameters)).isEqualTo(parameters);
+    }
+
+    @Test
+    public void isAntennaConnected_forTunerAdapter() throws Exception {
+        mTunerCallback.onAntennaState(/* connected= */ false);
+
+        assertWithMessage("Antenna connection status")
+                .that(mRadioTuner.isAntennaConnected()).isFalse();
+    }
+
+    @Test
+    public void hasControl_forTunerAdapter() throws Exception {
+        when(mTunerMock.isClosed()).thenReturn(true);
+
+        assertWithMessage("Control on tuner").that(mRadioTuner.hasControl()).isFalse();
+    }
+
+    @Test
+    public void onConfigurationChanged_forTunerCallbackAdapter() throws Exception {
+        mTunerCallback.onConfigurationChanged(TEST_BAND_CONFIG);
+
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS))
+                .onConfigurationChanged(TEST_BAND_CONFIG);
+    }
+
+    @Test
+    public void onTrafficAnnouncement_forTunerCallbackAdapter() throws Exception {
+        mTunerCallback.onTrafficAnnouncement(/* active= */ true);
+
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS))
+                .onTrafficAnnouncement(/* active= */ true);
+    }
+
+    @Test
+    public void onEmergencyAnnouncement_forTunerCallbackAdapter() throws Exception {
+        mTunerCallback.onEmergencyAnnouncement(/* active= */ true);
+
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS))
+                .onEmergencyAnnouncement(/* active= */ true);
+    }
+
+    @Test
+    public void onBackgroundScanAvailabilityChange_forTunerCallbackAdapter() throws Exception {
+        mTunerCallback.onBackgroundScanAvailabilityChange(/* isAvailable= */ false);
+
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS))
+                .onBackgroundScanAvailabilityChange(/* isAvailable= */ false);
+    }
+
+    @Test
+    public void onProgramListChanged_forTunerCallbackAdapter() throws Exception {
+        mTunerCallback.onProgramListChanged();
+
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramListChanged();
+    }
+
+    @Test
+    public void onParametersUpdated_forTunerCallbackAdapter() throws Exception {
+        Map<String, String> parametersExpected = Map.of("ParameterKeyMock", "ParameterValueMock");
+
+        mTunerCallback.onParametersUpdated(parametersExpected);
+
+        verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onParametersUpdated(parametersExpected);
+    }
+
+    private static RadioManager.ProgramInfo createFmProgramInfo() {
+        return new RadioManager.ProgramInfo(FM_SELECTOR, FM_IDENTIFIER, FM_IDENTIFIER,
+                /* relatedContent= */ null, /* infoFlags= */ 0b110001,
+                /* signalQuality= */ 1, createRadioMetadata(), /* vendorInfo= */ null);
+    }
+
+    private static RadioManager.FmBandConfig createBandConfig() {
+        return new RadioManager.FmBandConfig(new RadioManager.FmBandDescriptor(
+                RadioManager.REGION_ITU_1, RadioManager.BAND_FM, /* lowerLimit= */ 87500,
+                /* upperLimit= */ 108000, /* spacing= */ 200, /* stereo= */ true,
+                /* rds= */ false, /* ta= */ false, /* af= */ false, /* es= */ false));
+    }
+
+    private static RadioMetadata createRadioMetadata() {
+        RadioMetadata.Builder metadataBuilder = new RadioMetadata.Builder();
+        return metadataBuilder.putString(RadioMetadata.METADATA_KEY_ARTIST, "artistMock").build();
+    }
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
new file mode 100644
index 0000000..3119554
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
@@ -0,0 +1,221 @@
+/*
+ * 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.aidl;
+
+import android.hardware.broadcastradio.AmFmBandRange;
+import android.hardware.broadcastradio.AmFmRegionConfig;
+import android.hardware.broadcastradio.DabTableEntry;
+import android.hardware.broadcastradio.IdentifierType;
+import android.hardware.broadcastradio.Properties;
+import android.hardware.broadcastradio.VendorKeyValue;
+import android.hardware.radio.Announcement;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+
+import com.google.common.truth.Expect;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.Map;
+
+public final class ConversionUtilsTest {
+
+    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 String DAB_ENTRY_LABEL_1 = "5A";
+    private static final int DAB_ENTRY_FREQUENCY_1 = 174928;
+    private static final String DAB_ENTRY_LABEL_2 = "12D";
+    private static final int DAB_ENTRY_FREQUENCY_2 = 229072;
+    private static final String VENDOR_INFO_KEY_1 = "vendorKey1";
+    private static final String VENDOR_INFO_VALUE_1 = "vendorValue1";
+    private static final String VENDOR_INFO_KEY_2 = "vendorKey2";
+    private static final String VENDOR_INFO_VALUE_2 = "vendorValue2";
+    private static final String TEST_SERVICE_NAME = "serviceMock";
+    private static final int TEST_ID = 1;
+    private static final String TEST_MAKER = "makerMock";
+    private static final String TEST_PRODUCT = "productMock";
+    private static final String TEST_VERSION = "versionMock";
+    private static final String TEST_SERIAL = "serialMock";
+
+    private static final int TEST_ENABLED_TYPE = Announcement.TYPE_EMERGENCY;
+    private static final int TEST_ANNOUNCEMENT_FREQUENCY = FM_LOWER_LIMIT + FM_SPACING;
+
+    private static final RadioManager.ModuleProperties MODULE_PROPERTIES =
+            convertToModuleProperties();
+    private static final Announcement ANNOUNCEMENT =
+            ConversionUtils.announcementFromHalAnnouncement(
+                    AidlTestUtils.makeAnnouncement(TEST_ENABLED_TYPE, TEST_ANNOUNCEMENT_FREQUENCY));
+
+    @Rule
+    public final Expect expect = Expect.create();
+
+    @Test
+    public void propertiesFromHalProperties_idsMatch() {
+        expect.withMessage("Properties id")
+                .that(MODULE_PROPERTIES.getId()).isEqualTo(TEST_ID);
+    }
+
+    @Test
+    public void propertiesFromHalProperties_serviceNamesMatch() {
+        expect.withMessage("Service name")
+                .that(MODULE_PROPERTIES.getServiceName()).isEqualTo(TEST_SERVICE_NAME);
+    }
+
+    @Test
+    public void propertiesFromHalProperties_implementorsMatch() {
+        expect.withMessage("Implementor")
+                .that(MODULE_PROPERTIES.getImplementor()).isEqualTo(TEST_MAKER);
+    }
+
+
+    @Test
+    public void propertiesFromHalProperties_productsMatch() {
+        expect.withMessage("Product")
+                .that(MODULE_PROPERTIES.getProduct()).isEqualTo(TEST_PRODUCT);
+    }
+
+    @Test
+    public void propertiesFromHalProperties_versionsMatch() {
+        expect.withMessage("Version")
+                .that(MODULE_PROPERTIES.getVersion()).isEqualTo(TEST_VERSION);
+    }
+
+    @Test
+    public void propertiesFromHalProperties_serialsMatch() {
+        expect.withMessage("Serial")
+                .that(MODULE_PROPERTIES.getSerial()).isEqualTo(TEST_SERIAL);
+    }
+
+    @Test
+    public void propertiesFromHalProperties_dabTableInfoMatch() {
+        Map<String, Integer> dabTableExpected = Map.of(DAB_ENTRY_LABEL_1, DAB_ENTRY_FREQUENCY_1,
+                DAB_ENTRY_LABEL_2, DAB_ENTRY_FREQUENCY_2);
+
+        expect.withMessage("Supported program types")
+                .that(MODULE_PROPERTIES.getDabFrequencyTable())
+                .containsExactlyEntriesIn(dabTableExpected);
+    }
+
+    @Test
+    public void propertiesFromHalProperties_vendorInfoMatch() {
+        Map<String, String> vendorInfoExpected = Map.of(VENDOR_INFO_KEY_1, VENDOR_INFO_VALUE_1,
+                VENDOR_INFO_KEY_2, VENDOR_INFO_VALUE_2);
+
+        expect.withMessage("Vendor info").that(MODULE_PROPERTIES.getVendorInfo())
+                .containsExactlyEntriesIn(vendorInfoExpected);
+    }
+
+    @Test
+    public void propertiesFromHalProperties_bandsMatch() {
+        RadioManager.BandDescriptor[] bands = MODULE_PROPERTIES.getBands();
+
+        expect.withMessage("Band descriptors").that(bands).hasLength(2);
+
+        expect.withMessage("FM band frequency lower limit")
+                .that(bands[0].getLowerLimit()).isEqualTo(FM_LOWER_LIMIT);
+        expect.withMessage("FM band frequency upper limit")
+                .that(bands[0].getUpperLimit()).isEqualTo(FM_UPPER_LIMIT);
+        expect.withMessage("FM band frequency spacing")
+                .that(bands[0].getSpacing()).isEqualTo(FM_SPACING);
+
+        expect.withMessage("AM band frequency lower limit")
+                .that(bands[1].getLowerLimit()).isEqualTo(AM_LOWER_LIMIT);
+        expect.withMessage("AM band frequency upper limit")
+                .that(bands[1].getUpperLimit()).isEqualTo(AM_UPPER_LIMIT);
+        expect.withMessage("AM band frequency spacing")
+                .that(bands[1].getSpacing()).isEqualTo(AM_SPACING);
+    }
+
+    @Test
+    public void announcementFromHalAnnouncement_typesMatch() {
+        expect.withMessage("Announcement type")
+                .that(ANNOUNCEMENT.getType()).isEqualTo(TEST_ENABLED_TYPE);
+    }
+
+    @Test
+    public void announcementFromHalAnnouncement_selectorsMatch() {
+        ProgramSelector.Identifier primaryIdExpected = new ProgramSelector.Identifier(
+                ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, TEST_ANNOUNCEMENT_FREQUENCY);
+
+        ProgramSelector selector = ANNOUNCEMENT.getSelector();
+
+        expect.withMessage("Primary id of announcement selector")
+                .that(selector.getPrimaryId()).isEqualTo(primaryIdExpected);
+        expect.withMessage("Secondary ids of announcement selector")
+                .that(selector.getSecondaryIds()).isEmpty();
+    }
+
+    @Test
+    public void announcementFromHalAnnouncement_VendorInfoMatch() {
+        expect.withMessage("Announcement vendor info")
+                .that(ANNOUNCEMENT.getVendorInfo()).isEmpty();
+    }
+
+    private static RadioManager.ModuleProperties convertToModuleProperties() {
+        AmFmRegionConfig amFmConfig = createAmFmRegionConfig();
+        DabTableEntry[] dabTableEntries = new DabTableEntry[]{
+                createDabTableEntry(DAB_ENTRY_LABEL_1, DAB_ENTRY_FREQUENCY_1),
+                createDabTableEntry(DAB_ENTRY_LABEL_2, DAB_ENTRY_FREQUENCY_2)};
+        Properties properties = createHalProperties();
+
+        return ConversionUtils.propertiesFromHalProperties(TEST_ID, TEST_SERVICE_NAME, properties,
+                amFmConfig, dabTableEntries);
+    }
+
+    private static AmFmRegionConfig createAmFmRegionConfig() {
+        AmFmRegionConfig amFmRegionConfig = new AmFmRegionConfig();
+        amFmRegionConfig.ranges = new AmFmBandRange[]{
+                createAmFmBandRange(FM_LOWER_LIMIT, FM_UPPER_LIMIT, FM_SPACING),
+                createAmFmBandRange(AM_LOWER_LIMIT, AM_UPPER_LIMIT, AM_SPACING)};
+        return amFmRegionConfig;
+    }
+
+    private static AmFmBandRange createAmFmBandRange(int lowerBound, int upperBound, int spacing) {
+        AmFmBandRange bandRange = new AmFmBandRange();
+        bandRange.lowerBound = lowerBound;
+        bandRange.upperBound = upperBound;
+        bandRange.spacing = spacing;
+        bandRange.seekSpacing = bandRange.spacing;
+        return bandRange;
+    }
+
+    private static DabTableEntry createDabTableEntry(String label, int value) {
+        DabTableEntry dabTableEntry = new DabTableEntry();
+        dabTableEntry.label = label;
+        dabTableEntry.frequencyKhz = value;
+        return dabTableEntry;
+    }
+
+    private static Properties createHalProperties() {
+        Properties halProperties = new Properties();
+        halProperties.supportedIdentifierTypes = new int[]{IdentifierType.AMFM_FREQUENCY_KHZ,
+                IdentifierType.RDS_PI, IdentifierType.DAB_SID_EXT};
+        halProperties.maker = TEST_MAKER;
+        halProperties.product = TEST_PRODUCT;
+        halProperties.version = TEST_VERSION;
+        halProperties.serial = TEST_SERIAL;
+        halProperties.vendorInfo = new VendorKeyValue[]{
+                AidlTestUtils.makeVendorKeyValue(VENDOR_INFO_KEY_1, VENDOR_INFO_VALUE_1),
+                AidlTestUtils.makeVendorKeyValue(VENDOR_INFO_KEY_2, VENDOR_INFO_VALUE_2)};
+        return halProperties;
+    }
+}
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index a767f83..48cfc87 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -82,6 +82,7 @@
 
     resource_dirs: ["res"],
     resource_zips: [":FrameworksCoreTests_apks_as_resources"],
+    java_resources: [":ApkVerityTestCertDer"],
 
     data: [
         ":BstatsTestApp",
diff --git a/core/tests/coretests/OWNERS b/core/tests/coretests/OWNERS
index 0fb0c30..e8c9fe7 100644
--- a/core/tests/coretests/OWNERS
+++ b/core/tests/coretests/OWNERS
@@ -1 +1,4 @@
 include platform/frameworks/base:/services/core/java/com/android/server/am/OWNERS
+
+per-file BinderTest.java = file:platform/frameworks/native:/libs/binder/OWNERS
+per-file ParcelTest.java = file:platform/frameworks/native:/libs/binder/OWNERS
diff --git a/core/tests/coretests/res/raw/fsverity_sig b/core/tests/coretests/res/raw/fsverity_sig
new file mode 100644
index 0000000..b2f335d
--- /dev/null
+++ b/core/tests/coretests/res/raw/fsverity_sig
Binary files differ
diff --git a/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java b/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java
new file mode 100644
index 0000000..67b24ec
--- /dev/null
+++ b/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java
@@ -0,0 +1,277 @@
+/*
+ * 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 static android.app.backup.BackupRestoreEventLogger.OperationType.BACKUP;
+import static android.app.backup.BackupRestoreEventLogger.OperationType.RESTORE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.fail;
+
+import android.app.backup.BackupRestoreEventLogger.BackupRestoreDataType;
+import android.app.backup.BackupRestoreEventLogger.DataTypeResult;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class BackupRestoreEventLoggerTest {
+    private static final int DATA_TYPES_ALLOWED = 15;
+
+    private static final String DATA_TYPE_1 = "data_type_1";
+    private static final String DATA_TYPE_2 = "data_type_2";
+    private static final String ERROR_1 = "error_1";
+    private static final String ERROR_2 = "error_2";
+    private static final String METADATA_1 = "metadata_1";
+    private static final String METADATA_2 = "metadata_2";
+
+    private BackupRestoreEventLogger mLogger;
+    private MessageDigest mHashDigest;
+
+    @Before
+    public void setUp() throws Exception {
+        mHashDigest = MessageDigest.getInstance("SHA-256");
+    }
+
+    @Test
+    public void testBackupLogger_rejectsRestoreLogs() {
+        mLogger = new BackupRestoreEventLogger(BACKUP);
+
+        assertThat(mLogger.logItemsRestored(DATA_TYPE_1, /* count */ 5)).isFalse();
+        assertThat(mLogger.logItemsRestoreFailed(DATA_TYPE_1, /* count */ 5, ERROR_1)).isFalse();
+        assertThat(mLogger.logRestoreMetadata(DATA_TYPE_1, /* metadata */ "metadata")).isFalse();
+    }
+
+    @Test
+    public void testRestoreLogger_rejectsBackupLogs() {
+        mLogger = new BackupRestoreEventLogger(RESTORE);
+
+        assertThat(mLogger.logItemsBackedUp(DATA_TYPE_1, /* count */ 5)).isFalse();
+        assertThat(mLogger.logItemsBackupFailed(DATA_TYPE_1, /* count */ 5, ERROR_1)).isFalse();
+        assertThat(mLogger.logBackupMetaData(DATA_TYPE_1, /* metadata */ "metadata")).isFalse();
+    }
+
+    @Test
+    public void testBackupLogger_onlyAcceptsAllowedNumberOfDataTypes() {
+        mLogger = new BackupRestoreEventLogger(BACKUP);
+
+        for (int i = 0; i < DATA_TYPES_ALLOWED; i++) {
+            String dataType = DATA_TYPE_1 + i;
+            assertThat(mLogger.logItemsBackedUp(dataType, /* count */ 5)).isTrue();
+            assertThat(mLogger.logItemsBackupFailed(dataType, /* count */ 5, /* error */ null))
+                    .isTrue();
+            assertThat(mLogger.logBackupMetaData(dataType, METADATA_1)).isTrue();
+        }
+
+        assertThat(mLogger.logItemsBackedUp(DATA_TYPE_2, /* count */ 5)).isFalse();
+        assertThat(mLogger.logItemsBackupFailed(DATA_TYPE_2, /* count */ 5, /* error */ null))
+                .isFalse();
+        assertThat(mLogger.logRestoreMetadata(DATA_TYPE_2, METADATA_1)).isFalse();
+        assertThat(getResultForDataTypeIfPresent(mLogger, DATA_TYPE_2)).isEqualTo(Optional.empty());
+    }
+
+    @Test
+    public void testRestoreLogger_onlyAcceptsAllowedNumberOfDataTypes() {
+        mLogger = new BackupRestoreEventLogger(RESTORE);
+
+        for (int i = 0; i < DATA_TYPES_ALLOWED; i++) {
+            String dataType = DATA_TYPE_1 + i;
+            assertThat(mLogger.logItemsRestored(dataType, /* count */ 5)).isTrue();
+            assertThat(mLogger.logItemsRestoreFailed(dataType, /* count */ 5, /* error */ null))
+                    .isTrue();
+            assertThat(mLogger.logRestoreMetadata(dataType, METADATA_1)).isTrue();
+        }
+
+        assertThat(mLogger.logItemsRestored(DATA_TYPE_2, /* count */ 5)).isFalse();
+        assertThat(mLogger.logItemsRestoreFailed(DATA_TYPE_2, /* count */ 5, /* error */ null))
+                .isFalse();
+        assertThat(mLogger.logRestoreMetadata(DATA_TYPE_2, METADATA_1)).isFalse();
+        assertThat(getResultForDataTypeIfPresent(mLogger, DATA_TYPE_2)).isEqualTo(Optional.empty());
+    }
+
+    @Test
+    public void testLogBackupMetadata_repeatedCalls_recordsLatestMetadataHash() {
+        mLogger = new BackupRestoreEventLogger(BACKUP);
+
+        mLogger.logBackupMetaData(DATA_TYPE_1, METADATA_1);
+        mLogger.logBackupMetaData(DATA_TYPE_1, METADATA_2);
+
+        byte[] recordedHash = getResultForDataType(mLogger, DATA_TYPE_1).getMetadataHash();
+        byte[] expectedHash = getMetaDataHash(METADATA_2);
+        assertThat(Arrays.equals(recordedHash, expectedHash)).isTrue();
+    }
+
+    @Test
+    public void testLogRestoreMetadata_repeatedCalls_recordsLatestMetadataHash() {
+        mLogger = new BackupRestoreEventLogger(RESTORE);
+
+        mLogger.logRestoreMetadata(DATA_TYPE_1, METADATA_1);
+        mLogger.logRestoreMetadata(DATA_TYPE_1, METADATA_2);
+
+        byte[] recordedHash = getResultForDataType(mLogger, DATA_TYPE_1).getMetadataHash();
+        byte[] expectedHash = getMetaDataHash(METADATA_2);
+        assertThat(Arrays.equals(recordedHash, expectedHash)).isTrue();
+    }
+
+    @Test
+    public void testLogItemsBackedUp_repeatedCalls_recordsTotalItems() {
+        mLogger = new BackupRestoreEventLogger(BACKUP);
+
+        int firstCount = 10;
+        int secondCount = 5;
+        mLogger.logItemsBackedUp(DATA_TYPE_1, firstCount);
+        mLogger.logItemsBackedUp(DATA_TYPE_1, secondCount);
+
+        int dataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getSuccessCount();
+        assertThat(dataTypeCount).isEqualTo(firstCount + secondCount);
+    }
+
+    @Test
+    public void testLogItemsRestored_repeatedCalls_recordsTotalItems() {
+        mLogger = new BackupRestoreEventLogger(RESTORE);
+
+        int firstCount = 10;
+        int secondCount = 5;
+        mLogger.logItemsRestored(DATA_TYPE_1, firstCount);
+        mLogger.logItemsRestored(DATA_TYPE_1, secondCount);
+
+        int dataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getSuccessCount();
+        assertThat(dataTypeCount).isEqualTo(firstCount + secondCount);
+    }
+
+    @Test
+    public void testLogItemsBackedUp_multipleDataTypes_recordsEachDataType() {
+        mLogger = new BackupRestoreEventLogger(BACKUP);
+
+        int firstCount = 10;
+        int secondCount = 5;
+        mLogger.logItemsBackedUp(DATA_TYPE_1, firstCount);
+        mLogger.logItemsBackedUp(DATA_TYPE_2, secondCount);
+
+        int firstDataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getSuccessCount();
+        int secondDataTypeCount = getResultForDataType(mLogger, DATA_TYPE_2).getSuccessCount();
+        assertThat(firstDataTypeCount).isEqualTo(firstCount);
+        assertThat(secondDataTypeCount).isEqualTo(secondCount);
+    }
+
+    @Test
+    public void testLogItemsRestored_multipleDataTypes_recordsEachDataType() {
+        mLogger = new BackupRestoreEventLogger(RESTORE);
+
+        int firstCount = 10;
+        int secondCount = 5;
+        mLogger.logItemsRestored(DATA_TYPE_1, firstCount);
+        mLogger.logItemsRestored(DATA_TYPE_2, secondCount);
+
+        int firstDataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getSuccessCount();
+        int secondDataTypeCount = getResultForDataType(mLogger, DATA_TYPE_2).getSuccessCount();
+        assertThat(firstDataTypeCount).isEqualTo(firstCount);
+        assertThat(secondDataTypeCount).isEqualTo(secondCount);
+    }
+
+    @Test
+    public void testLogItemsBackupFailed_repeatedCalls_recordsTotalItems() {
+        mLogger = new BackupRestoreEventLogger(BACKUP);
+
+        int firstCount = 10;
+        int secondCount = 5;
+        mLogger.logItemsBackupFailed(DATA_TYPE_1, firstCount, /* error */ null);
+        mLogger.logItemsBackupFailed(DATA_TYPE_1, secondCount, "error");
+
+        int dataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getFailCount();
+        assertThat(dataTypeCount).isEqualTo(firstCount + secondCount);
+    }
+
+    @Test
+    public void testLogItemsRestoreFailed_repeatedCalls_recordsTotalItems() {
+        mLogger = new BackupRestoreEventLogger(RESTORE);
+
+        int firstCount = 10;
+        int secondCount = 5;
+        mLogger.logItemsRestoreFailed(DATA_TYPE_1, firstCount, /* error */ null);
+        mLogger.logItemsRestoreFailed(DATA_TYPE_1, secondCount, "error");
+
+        int dataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getFailCount();
+        assertThat(dataTypeCount).isEqualTo(firstCount + secondCount);
+    }
+
+    @Test
+    public void testLogItemsBackupFailed_multipleErrors_recordsEachError() {
+        mLogger = new BackupRestoreEventLogger(BACKUP);
+
+        int firstCount = 10;
+        int secondCount = 5;
+        mLogger.logItemsBackupFailed(DATA_TYPE_1, firstCount, ERROR_1);
+        mLogger.logItemsBackupFailed(DATA_TYPE_1, secondCount, ERROR_2);
+
+        int firstErrorTypeCount = getResultForDataType(mLogger, DATA_TYPE_1)
+                .getErrors().get(ERROR_1);
+        int secondErrorTypeCount = getResultForDataType(mLogger, DATA_TYPE_1)
+                .getErrors().get(ERROR_2);
+        assertThat(firstErrorTypeCount).isEqualTo(firstCount);
+        assertThat(secondErrorTypeCount).isEqualTo(secondCount);
+    }
+
+    @Test
+    public void testLogItemsRestoreFailed_multipleErrors_recordsEachError() {
+        mLogger = new BackupRestoreEventLogger(RESTORE);
+
+        int firstCount = 10;
+        int secondCount = 5;
+        mLogger.logItemsRestoreFailed(DATA_TYPE_1, firstCount, ERROR_1);
+        mLogger.logItemsRestoreFailed(DATA_TYPE_1, secondCount, ERROR_2);
+
+        int firstErrorTypeCount = getResultForDataType(mLogger, DATA_TYPE_1)
+                .getErrors().get(ERROR_1);
+        int secondErrorTypeCount = getResultForDataType(mLogger, DATA_TYPE_1)
+                .getErrors().get(ERROR_2);
+        assertThat(firstErrorTypeCount).isEqualTo(firstCount);
+        assertThat(secondErrorTypeCount).isEqualTo(secondCount);
+    }
+
+    private static DataTypeResult getResultForDataType(BackupRestoreEventLogger logger,
+            @BackupRestoreDataType String dataType) {
+        Optional<DataTypeResult> result = getResultForDataTypeIfPresent(logger, dataType);
+        if (result.isEmpty()) {
+            fail("Failed to find result for data type: " + dataType);
+        }
+        return result.get();
+    }
+
+    private static Optional<DataTypeResult> getResultForDataTypeIfPresent(
+            BackupRestoreEventLogger logger, @BackupRestoreDataType String dataType) {
+        List<DataTypeResult> resultList = logger.getLoggingResults();
+        return resultList.stream().filter(
+                dataTypeResult -> dataTypeResult.getDataType().equals(dataType)).findAny();
+    }
+
+    private byte[] getMetaDataHash(String metaData) {
+        return mHashDigest.digest(metaData.getBytes(StandardCharsets.UTF_8));
+    }
+}
diff --git a/core/tests/coretests/src/android/app/backup/OWNERS b/core/tests/coretests/src/android/app/backup/OWNERS
new file mode 100644
index 0000000..53b6c78
--- /dev/null
+++ b/core/tests/coretests/src/android/app/backup/OWNERS
@@ -0,0 +1 @@
+include /services/backup/OWNERS
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java
index fdd278b..e2fe87b4 100644
--- a/core/tests/coretests/src/android/os/ParcelTest.java
+++ b/core/tests/coretests/src/android/os/ParcelTest.java
@@ -37,6 +37,13 @@
     private static final String INTERFACE_TOKEN_2 = "Another IBinder interface token";
 
     @Test
+    public void testIsForRpc() {
+        Parcel p = Parcel.obtain();
+        assertEquals(false, p.isForRpc());
+        p.recycle();
+    }
+
+    @Test
     public void testCallingWorkSourceUidAfterWrite() {
         Parcel p = Parcel.obtain();
         // Method does not throw if replaceCallingWorkSourceUid is called before requests headers
diff --git a/core/tests/coretests/src/android/text/method/BackspaceTest.java b/core/tests/coretests/src/android/text/method/BackspaceTest.java
index ddae652..19c2c61 100644
--- a/core/tests/coretests/src/android/text/method/BackspaceTest.java
+++ b/core/tests/coretests/src/android/text/method/BackspaceTest.java
@@ -193,11 +193,15 @@
         backspace(state, 0);
         state.assertEquals("|");
 
-        // Emoji modifier can be appended to the first emoji.
+        // Emoji modifier can be appended to each emoji.
         state.setByString("U+1F469 U+1F3FB U+200D U+1F4BC |");
         backspace(state, 0);
         state.assertEquals("|");
 
+        state.setByString("U+1F468 U+1F3FF U+200D U+2764 U+FE0F U+200D U+1F468 U+1F3FB |");
+        backspace(state, 0);
+        state.assertEquals("|");
+
         // End with ZERO WIDTH JOINER
         state.setByString("U+1F441 U+200D |");
         backspace(state, 0);
diff --git a/core/tests/coretests/src/com/android/internal/security/ContentSignerWrapper.java b/core/tests/coretests/src/com/android/internal/security/ContentSignerWrapper.java
new file mode 100644
index 0000000..0254afe
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/security/ContentSignerWrapper.java
@@ -0,0 +1,46 @@
+/*
+ * 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.security;
+
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.operator.ContentSigner;
+
+import java.io.OutputStream;
+
+/** A wrapper class of ContentSigner */
+class ContentSignerWrapper implements ContentSigner {
+    private final ContentSigner mSigner;
+
+    ContentSignerWrapper(ContentSigner wrapped) {
+        mSigner = wrapped;
+    }
+
+    @Override
+    public AlgorithmIdentifier getAlgorithmIdentifier() {
+        return mSigner.getAlgorithmIdentifier();
+    }
+
+    @Override
+    public OutputStream getOutputStream() {
+        return mSigner.getOutputStream();
+    }
+
+    @Override
+    public byte[] getSignature() {
+        return mSigner.getSignature();
+    }
+}
diff --git a/core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java b/core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java
new file mode 100644
index 0000000..1513654
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java
@@ -0,0 +1,355 @@
+/*
+ * 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.security;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.frameworks.coretests.R;
+
+import org.bouncycastle.asn1.ASN1Encoding;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
+import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cms.CMSException;
+import org.bouncycastle.cms.CMSProcessableByteArray;
+import org.bouncycastle.cms.CMSSignedData;
+import org.bouncycastle.cms.CMSSignedDataGenerator;
+import org.bouncycastle.cms.SignerInfoGenerator;
+import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.DigestCalculator;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.bc.BcDigestCalculatorProvider;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HexFormat;
+
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class VerityUtilsTest {
+    private static final byte[] SAMPLE_DIGEST = "12345678901234567890123456789012".getBytes();
+    private static final byte[] FORMATTED_SAMPLE_DIGEST = toFormattedDigest(SAMPLE_DIGEST);
+
+    KeyPair mKeyPair;
+    ContentSigner mContentSigner;
+    X509CertificateHolder mCertificateHolder;
+    byte[] mCertificateDerEncoded;
+
+    @Before
+    public void setUp() throws Exception {
+        mKeyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
+        mContentSigner = newFsverityContentSigner(mKeyPair.getPrivate());
+        mCertificateHolder =
+                newX509CertificateHolder(mContentSigner, mKeyPair.getPublic(), "Someone");
+        mCertificateDerEncoded = mCertificateHolder.getEncoded();
+    }
+
+    @Test
+    public void testOnlyAcceptCorrectDigest() throws Exception {
+        byte[] pkcs7Signature =
+                generatePkcs7Signature(mContentSigner, mCertificateHolder, FORMATTED_SAMPLE_DIGEST);
+
+        byte[] anotherDigest = Arrays.copyOf(SAMPLE_DIGEST, SAMPLE_DIGEST.length);
+        anotherDigest[0] ^= (byte) 1;
+
+        assertTrue(verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded));
+        assertFalse(verifySignature(pkcs7Signature, anotherDigest, mCertificateDerEncoded));
+    }
+
+    @Test
+    public void testDigestWithWrongSize() throws Exception {
+        byte[] pkcs7Signature =
+                generatePkcs7Signature(mContentSigner, mCertificateHolder, FORMATTED_SAMPLE_DIGEST);
+        assertTrue(verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded));
+
+        byte[] digestTooShort = Arrays.copyOfRange(SAMPLE_DIGEST, 0, SAMPLE_DIGEST.length - 1);
+        assertFalse(verifySignature(pkcs7Signature, digestTooShort, mCertificateDerEncoded));
+
+        byte[] digestTooLong = Arrays.copyOfRange(SAMPLE_DIGEST, 0, SAMPLE_DIGEST.length + 1);
+        assertFalse(verifySignature(pkcs7Signature, digestTooLong, mCertificateDerEncoded));
+    }
+
+    @Test
+    public void testOnlyAcceptGoodSignature() throws Exception {
+        byte[] pkcs7Signature =
+                generatePkcs7Signature(mContentSigner, mCertificateHolder, FORMATTED_SAMPLE_DIGEST);
+
+        byte[] anotherDigest = Arrays.copyOf(SAMPLE_DIGEST, SAMPLE_DIGEST.length);
+        anotherDigest[0] ^= (byte) 1;
+        byte[] anotherPkcs7Signature =
+                generatePkcs7Signature(
+                        mContentSigner, mCertificateHolder, toFormattedDigest(anotherDigest));
+
+        assertTrue(verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded));
+        assertFalse(verifySignature(anotherPkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded));
+    }
+
+    @Test
+    public void testOnlyValidCertCanVerify() throws Exception {
+        byte[] pkcs7Signature =
+                generatePkcs7Signature(mContentSigner, mCertificateHolder, FORMATTED_SAMPLE_DIGEST);
+
+        var wrongKeyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
+        var wrongContentSigner = newFsverityContentSigner(wrongKeyPair.getPrivate());
+        var wrongCertificateHolder =
+                newX509CertificateHolder(wrongContentSigner, wrongKeyPair.getPublic(), "Not Me");
+        byte[] wrongCertificateDerEncoded = wrongCertificateHolder.getEncoded();
+
+        assertFalse(verifySignature(pkcs7Signature, SAMPLE_DIGEST, wrongCertificateDerEncoded));
+    }
+
+    @Test
+    public void testRejectSignatureWithContent() throws Exception {
+        CMSSignedDataGenerator generator =
+                newFsveritySignedDataGenerator(mContentSigner, mCertificateHolder);
+        byte[] pkcs7SignatureNonDetached =
+                generatePkcs7SignatureInternal(
+                        generator, FORMATTED_SAMPLE_DIGEST, /* encapsulate */ true);
+
+        assertFalse(
+                verifySignature(pkcs7SignatureNonDetached, SAMPLE_DIGEST, mCertificateDerEncoded));
+    }
+
+    @Test
+    public void testRejectSignatureWithCertificate() throws Exception {
+        CMSSignedDataGenerator generator =
+                newFsveritySignedDataGenerator(mContentSigner, mCertificateHolder);
+        generator.addCertificate(mCertificateHolder);
+        byte[] pkcs7Signature =
+                generatePkcs7SignatureInternal(
+                        generator, FORMATTED_SAMPLE_DIGEST, /* encapsulate */ false);
+
+        assertFalse(
+                verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded));
+    }
+
+    @Ignore("No easy way to construct test data")
+    @Test
+    public void testRejectSignatureWithCRL() throws Exception {
+        CMSSignedDataGenerator generator =
+                newFsveritySignedDataGenerator(mContentSigner, mCertificateHolder);
+
+        // The current bouncycastle version does not have an easy way to generate a CRL.
+        // TODO: enable the test once this is doable, e.g. with X509v2CRLBuilder.
+        // generator.addCRL(new X509CRLHolder(CertificateList.getInstance(new DERSequence(...))));
+        byte[] pkcs7Signature =
+                generatePkcs7SignatureInternal(
+                        generator, FORMATTED_SAMPLE_DIGEST, /* encapsulate */ false);
+
+        assertFalse(
+                verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded));
+    }
+
+    @Test
+    public void testRejectUnsupportedSignatureAlgorithms() throws Exception {
+        var contentSigner = newFsverityContentSigner(mKeyPair.getPrivate(), "MD5withRSA", null);
+        var certificateHolder =
+                newX509CertificateHolder(contentSigner, mKeyPair.getPublic(), "Someone");
+        byte[] pkcs7Signature =
+                generatePkcs7Signature(contentSigner, certificateHolder, FORMATTED_SAMPLE_DIGEST);
+
+        assertFalse(verifySignature(pkcs7Signature, SAMPLE_DIGEST, certificateHolder.getEncoded()));
+    }
+
+    @Test
+    public void testRejectUnsupportedDigestAlgorithm() throws Exception {
+        CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
+        generator.addSignerInfoGenerator(
+                newSignerInfoGenerator(
+                        mContentSigner,
+                        mCertificateHolder,
+                        OIWObjectIdentifiers.idSHA1,
+                        true)); // directSignature
+        byte[] pkcs7Signature =
+                generatePkcs7SignatureInternal(
+                        generator, FORMATTED_SAMPLE_DIGEST, /* encapsulate */ false);
+
+        assertFalse(verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded));
+    }
+
+    @Test
+    public void testRejectAnySignerInfoAttributes() throws Exception {
+        var generator = new CMSSignedDataGenerator();
+        generator.addSignerInfoGenerator(
+                newSignerInfoGenerator(
+                        mContentSigner,
+                        mCertificateHolder,
+                        NISTObjectIdentifiers.id_sha256,
+                        false)); // directSignature
+        byte[] pkcs7Signature =
+                generatePkcs7SignatureInternal(
+                        generator, FORMATTED_SAMPLE_DIGEST, /* encapsulate */ false);
+
+        assertFalse(verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded));
+    }
+
+    @Test
+    public void testSignatureGeneratedExternally() throws Exception {
+        var context = InstrumentationRegistry.getInstrumentation().getContext();
+        byte[] cert = getClass().getClassLoader().getResourceAsStream("ApkVerityTestCert.der")
+                .readAllBytes();
+        // The signature is generated by:
+        //   fsverity sign <(echo -n fs-verity) fsverity_sig --key=ApkVerityTestKey.pem \
+        //   --cert=ApkVerityTestCert.pem
+        byte[] sig = context.getResources().openRawResource(R.raw.fsverity_sig).readAllBytes();
+        // The fs-verity digest is generated by:
+        //   fsverity digest --compact <(echo -n fs-verity)
+        byte[] digest = HexFormat.of().parseHex(
+                "3d248ca542a24fc62d1c43b916eae5016878e2533c88238480b26128a1f1af95");
+
+        assertTrue(verifySignature(sig, digest, cert));
+    }
+
+    private static boolean verifySignature(
+            byte[] pkcs7Signature, byte[] fsverityDigest, byte[] certificateDerEncoded) {
+        return VerityUtils.verifyPkcs7DetachedSignature(
+                pkcs7Signature, fsverityDigest, new ByteArrayInputStream(certificateDerEncoded));
+    }
+
+    private static byte[] toFormattedDigest(byte[] digest) {
+        return VerityUtils.toFormattedDigest(digest);
+    }
+
+    private static byte[] generatePkcs7Signature(
+            ContentSigner contentSigner, X509CertificateHolder certificateHolder, byte[] signedData)
+            throws IOException, CMSException, OperatorCreationException {
+        CMSSignedDataGenerator generator =
+                newFsveritySignedDataGenerator(contentSigner, certificateHolder);
+        return generatePkcs7SignatureInternal(generator, signedData, /* encapsulate */ false);
+    }
+
+    private static byte[] generatePkcs7SignatureInternal(
+            CMSSignedDataGenerator generator, byte[] signedData, boolean encapsulate)
+            throws IOException, CMSException, OperatorCreationException {
+        CMSSignedData cmsSignedData =
+                generator.generate(new CMSProcessableByteArray(signedData), encapsulate);
+        return cmsSignedData.toASN1Structure().getEncoded(ASN1Encoding.DL);
+    }
+
+    private static CMSSignedDataGenerator newFsveritySignedDataGenerator(
+            ContentSigner contentSigner, X509CertificateHolder certificateHolder)
+            throws IOException, CMSException, OperatorCreationException {
+        var generator = new CMSSignedDataGenerator();
+        generator.addSignerInfoGenerator(
+                newSignerInfoGenerator(
+                        contentSigner,
+                        certificateHolder,
+                        NISTObjectIdentifiers.id_sha256,
+                        true)); // directSignature
+        return generator;
+    }
+
+    private static SignerInfoGenerator newSignerInfoGenerator(
+            ContentSigner contentSigner,
+            X509CertificateHolder certificateHolder,
+            ASN1ObjectIdentifier digestAlgorithmId,
+            boolean directSignature)
+            throws IOException, CMSException, OperatorCreationException {
+        var provider =
+                new BcDigestCalculatorProvider() {
+                    /**
+                     * Allow the caller to override the digest algorithm, especially when the
+                     * default does not work (i.e. BcDigestCalculatorProvider could return null).
+                     *
+                     * <p>For example, the current fs-verity signature has to use rsaEncryption for
+                     * the signature algorithm, but BcDigestCalculatorProvider will return null,
+                     * thus we need a way to override.
+                     *
+                     * <p>TODO: After bouncycastle 1.70, we can remove this override and just use
+                     * {@code JcaSignerInfoGeneratorBuilder#setContentDigest}.
+                     */
+                    @Override
+                    public DigestCalculator get(AlgorithmIdentifier algorithm)
+                            throws OperatorCreationException {
+                        return super.get(new AlgorithmIdentifier(digestAlgorithmId));
+                    }
+                };
+        var builder =
+                new JcaSignerInfoGeneratorBuilder(provider).setDirectSignature(directSignature);
+        return builder.build(contentSigner, certificateHolder);
+    }
+
+    private static ContentSigner newFsverityContentSigner(PrivateKey privateKey)
+            throws OperatorCreationException {
+        // fs-verity expects the signature to have rsaEncryption as the exact algorithm, so
+        // override the default.
+        return newFsverityContentSigner(
+                privateKey, "SHA256withRSA", PKCSObjectIdentifiers.rsaEncryption);
+    }
+
+    private static ContentSigner newFsverityContentSigner(
+            PrivateKey privateKey,
+            String signatureAlgorithm,
+            ASN1ObjectIdentifier signatureAlgorithmIdOverride)
+            throws OperatorCreationException {
+        if (signatureAlgorithmIdOverride != null) {
+            return new ContentSignerWrapper(
+                    new JcaContentSignerBuilder(signatureAlgorithm).build(privateKey)) {
+                @Override
+                public AlgorithmIdentifier getAlgorithmIdentifier() {
+                    return new AlgorithmIdentifier(signatureAlgorithmIdOverride);
+                }
+            };
+        } else {
+            return new JcaContentSignerBuilder(signatureAlgorithm).build(privateKey);
+        }
+    }
+
+    private static X509CertificateHolder newX509CertificateHolder(
+            ContentSigner contentSigner, PublicKey publicKey, String name) {
+        // Time doesn't really matter, as we only care about the key.
+        Instant now = Instant.now();
+
+        return new X509v3CertificateBuilder(
+                        new X500Name("CN=Issuer " + name),
+                        /* serial= */ BigInteger.valueOf(now.getEpochSecond()),
+                        new Date(now.minus(Duration.ofDays(1)).toEpochMilli()),
+                        new Date(now.plus(Duration.ofDays(1)).toEpochMilli()),
+                        new X500Name("CN=Subject " + name),
+                        SubjectPublicKeyInfo.getInstance(publicKey.getEncoded()))
+                .build(contentSigner);
+    }
+}
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 30c3d50..df5f921 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -23,6 +23,10 @@
          TODO(b/238217847): This config is temporary until we refactor the base WMComponent. -->
     <bool name="config_registerShellTaskOrganizerOnInit">true</bool>
 
+    <!-- Determines whether to register the shell transitions on init.
+         TODO(b/238217847): This config is temporary until we refactor the base WMComponent. -->
+    <bool name="config_registerShellTransitionsOnInit">true</bool>
+
     <!-- Animation duration for PIP when entering. -->
     <integer name="config_pipEnterAnimationDuration">425</integer>
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
index 48c5f64..bd2ea9c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
@@ -41,7 +41,6 @@
 import android.window.WindowContainerTransaction;
 
 import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.transition.Transitions;
 
 import java.io.PrintWriter;
 import java.util.concurrent.Executor;
@@ -122,7 +121,7 @@
 
     /** Until all users are converted, we may have mixed-use (eg. Car). */
     private boolean isUsingShellTransitions() {
-        return mTaskViewTransitions != null && Transitions.ENABLE_SHELL_TRANSITIONS;
+        return mTaskViewTransitions != null && mTaskViewTransitions.isEnabled();
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java
index 83335ac..07d5012 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java
@@ -87,6 +87,10 @@
         // Note: Don't unregister handler since this is a singleton with lifetime bound to Shell
     }
 
+    boolean isEnabled() {
+        return mTransitions.isRegistered();
+    }
+
     /**
      * Looks through the pending transitions for one matching `taskView`.
      * @param taskView the pending transition should be for this.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 64dbfbb..8b8e192 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -507,6 +507,10 @@
             @ShellMainThread ShellExecutor mainExecutor,
             @ShellMainThread Handler mainHandler,
             @ShellAnimationThread ShellExecutor animExecutor) {
+        if (!context.getResources().getBoolean(R.bool.config_registerShellTransitionsOnInit)) {
+            // TODO(b/238217847): Force override shell init if registration is disabled
+            shellInit = new ShellInit(mainExecutor);
+        }
         return new Transitions(context, shellInit, shellController, organizer, pool,
                 displayController, mainExecutor, mainHandler, animExecutor);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
index 195ff50..2fafe67 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
@@ -48,7 +48,6 @@
         try {
             int result = Settings.System.getIntForUser(context.getContentResolver(),
                     Settings.System.DESKTOP_MODE, UserHandle.USER_CURRENT);
-            ProtoLog.d(WM_SHELL_DESKTOP_MODE, "isDesktopModeEnabled=%s", result);
             return result != 0;
         } catch (Exception e) {
             ProtoLog.e(WM_SHELL_DESKTOP_MODE, "Failed to read DESKTOP_MODE setting %s", e);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index c91d54a..b7749fc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -69,22 +69,28 @@
 
     /**
      * Mark a task with given [taskId] as active.
+     *
+     * @return `true` if the task was not active
      */
-    fun addActiveTask(taskId: Int) {
+    fun addActiveTask(taskId: Int): Boolean {
         val added = activeTasks.add(taskId)
         if (added) {
             activeTasksListeners.onEach { it.onActiveTasksChanged() }
         }
+        return added
     }
 
     /**
      * Remove task with given [taskId] from active tasks.
+     *
+     * @return `true` if the task was active
      */
-    fun removeActiveTask(taskId: Int) {
+    fun removeActiveTask(taskId: Int): Boolean {
         val removed = activeTasks.remove(taskId)
         if (removed) {
             activeTasksListeners.onEach { it.onActiveTasksChanged() }
         }
+        return removed
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index eaa7158..90b35a5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -87,11 +87,13 @@
         }
 
         if (DesktopModeStatus.IS_SUPPORTED && taskInfo.isVisible) {
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
-                    "Adding active freeform task: #%d", taskInfo.taskId);
-            mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId));
-            mDesktopModeTaskRepository.ifPresent(
-                    it -> it.updateVisibleFreeformTasks(taskInfo.taskId, true));
+            mDesktopModeTaskRepository.ifPresent(repository -> {
+                if (repository.addActiveTask(taskInfo.taskId)) {
+                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+                            "Adding active freeform task: #%d", taskInfo.taskId);
+                }
+                repository.updateVisibleFreeformTasks(taskInfo.taskId, true);
+            });
         }
     }
 
@@ -102,11 +104,13 @@
         mTasks.remove(taskInfo.taskId);
 
         if (DesktopModeStatus.IS_SUPPORTED) {
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
-                    "Removing active freeform task: #%d", taskInfo.taskId);
-            mDesktopModeTaskRepository.ifPresent(it -> it.removeActiveTask(taskInfo.taskId));
-            mDesktopModeTaskRepository.ifPresent(
-                    it -> it.updateVisibleFreeformTasks(taskInfo.taskId, false));
+            mDesktopModeTaskRepository.ifPresent(repository -> {
+                if (repository.removeActiveTask(taskInfo.taskId)) {
+                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+                            "Removing active freeform task: #%d", taskInfo.taskId);
+                }
+                repository.updateVisibleFreeformTasks(taskInfo.taskId, false);
+            });
         }
 
         if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
@@ -123,13 +127,15 @@
         mWindowDecorationViewModel.onTaskInfoChanged(state.mTaskInfo);
 
         if (DesktopModeStatus.IS_SUPPORTED) {
-            if (taskInfo.isVisible) {
-                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
-                        "Adding active freeform task: #%d", taskInfo.taskId);
-                mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId));
-            }
-            mDesktopModeTaskRepository.ifPresent(
-                    it -> it.updateVisibleFreeformTasks(taskInfo.taskId, taskInfo.isVisible));
+            mDesktopModeTaskRepository.ifPresent(repository -> {
+                if (taskInfo.isVisible) {
+                    if (repository.addActiveTask(taskInfo.taskId)) {
+                        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+                                "Adding active freeform task: #%d", taskInfo.taskId);
+                    }
+                }
+                repository.updateVisibleFreeformTasks(taskInfo.taskId, taskInfo.isVisible);
+            });
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
index b9746e3..cbed4b5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
@@ -115,8 +115,8 @@
         // coordinates so offset the bounds to 0,0
         mTmpDestinationRect.offsetTo(0, 0);
         mTmpDestinationRect.inset(insets);
-        // Scale by the shortest edge and offset such that the top/left of the scaled inset source
-        // rect aligns with the top/left of the destination bounds
+        // Scale to the bounds no smaller than the destination and offset such that the top/left
+        // of the scaled inset source rect aligns with the top/left of the destination bounds
         final float scale;
         if (isInPipDirection
                 && sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) {
@@ -129,9 +129,8 @@
                     : (float) destinationBounds.height() / sourceBounds.height();
             scale = (1 - fraction) * startScale + fraction * endScale;
         } else {
-            scale = sourceBounds.width() <= sourceBounds.height()
-                    ? (float) destinationBounds.width() / sourceBounds.width()
-                    : (float) destinationBounds.height() / sourceBounds.height();
+            scale = Math.max((float) destinationBounds.width() / sourceBounds.width(),
+                    (float) destinationBounds.height() / sourceBounds.height());
         }
         final float left = destinationBounds.left - insets.left * scale;
         final float top = destinationBounds.top - insets.top * scale;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index d7ca791..21a1310 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -108,6 +108,14 @@
     private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction t, @NonNull WindowContainerToken mainRoot,
             @NonNull WindowContainerToken sideRoot, @NonNull WindowContainerToken topRoot) {
+        final TransitSession pendingTransition = getPendingTransition(transition);
+        if (pendingTransition != null && pendingTransition.mCanceled) {
+            // The pending transition was canceled, so skip playing animation.
+            t.apply();
+            onFinish(null /* wct */, null /* wctCB */);
+            return;
+        }
+
         // Play some place-holder fade animations
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
@@ -170,9 +178,7 @@
     }
 
     boolean isPendingTransition(IBinder transition) {
-        return isPendingEnter(transition)
-                || isPendingDismiss(transition)
-                || isPendingRecent(transition);
+        return getPendingTransition(transition) != null;
     }
 
     boolean isPendingEnter(IBinder transition) {
@@ -187,22 +193,38 @@
         return mPendingDismiss != null && mPendingDismiss.mTransition == transition;
     }
 
+    @Nullable
+    private TransitSession getPendingTransition(IBinder transition) {
+        if (isPendingEnter(transition)) {
+            return mPendingEnter;
+        } else if (isPendingRecent(transition)) {
+            return mPendingRecent;
+        } else if (isPendingDismiss(transition)) {
+            return mPendingDismiss;
+        }
+
+        return null;
+    }
+
     /** Starts a transition to enter split with a remote transition animator. */
     IBinder startEnterTransition(
             @WindowManager.TransitionType int transitType,
             WindowContainerTransaction wct,
             @Nullable RemoteTransition remoteTransition,
             Transitions.TransitionHandler handler,
-            @Nullable TransitionCallback callback) {
+            @Nullable TransitionConsumedCallback consumedCallback,
+            @Nullable TransitionFinishedCallback finishedCallback) {
         final IBinder transition = mTransitions.startTransition(transitType, wct, handler);
-        setEnterTransition(transition, remoteTransition, callback);
+        setEnterTransition(transition, remoteTransition, consumedCallback, finishedCallback);
         return transition;
     }
 
     /** Sets a transition to enter split. */
     void setEnterTransition(@NonNull IBinder transition,
-            @Nullable RemoteTransition remoteTransition, @Nullable TransitionCallback callback) {
-        mPendingEnter = new TransitSession(transition, callback);
+            @Nullable RemoteTransition remoteTransition,
+            @Nullable TransitionConsumedCallback consumedCallback,
+            @Nullable TransitionFinishedCallback finishedCallback) {
+        mPendingEnter = new TransitSession(transition, consumedCallback, finishedCallback);
 
         if (remoteTransition != null) {
             // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff)
@@ -237,8 +259,9 @@
     }
 
     void setRecentTransition(@NonNull IBinder transition,
-            @Nullable RemoteTransition remoteTransition, @Nullable TransitionCallback callback) {
-        mPendingRecent = new TransitSession(transition, callback);
+            @Nullable RemoteTransition remoteTransition,
+            @Nullable TransitionFinishedCallback finishCallback) {
+        mPendingRecent = new TransitSession(transition, null /* consumedCb */, finishCallback);
 
         if (remoteTransition != null) {
             // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff)
@@ -248,7 +271,7 @@
         }
 
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  splitTransition "
-                        + " deduced Enter recent panel");
+                + " deduced Enter recent panel");
     }
 
     void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t,
@@ -256,14 +279,9 @@
         if (mergeTarget != mAnimatingTransition) return;
 
         if (isPendingEnter(transition) && isPendingRecent(mergeTarget)) {
-            mPendingRecent.mCallback = new TransitionCallback() {
-                @Override
-                public void onTransitionFinished(WindowContainerTransaction finishWct,
-                        SurfaceControl.Transaction finishT) {
-                    // Since there's an entering transition merged, recent transition no longer
-                    // need to handle entering split screen after the transition finished.
-                }
-            };
+            // Since there's an entering transition merged, recent transition no longer
+            // need to handle entering split screen after the transition finished.
+            mPendingRecent.setFinishedCallback(null);
         }
 
         if (mActiveRemoteHandler != null) {
@@ -277,7 +295,7 @@
     }
 
     boolean end() {
-        // If its remote, there's nothing we can do right now.
+        // If It's remote, there's nothing we can do right now.
         if (mActiveRemoteHandler != null) return false;
         for (int i = mAnimations.size() - 1; i >= 0; --i) {
             final Animator anim = mAnimations.get(i);
@@ -290,20 +308,20 @@
             @Nullable SurfaceControl.Transaction finishT) {
         if (isPendingEnter(transition)) {
             if (!aborted) {
-                // An enter transition got merged, appends the rest operations to finish entering
+                // An entering transition got merged, appends the rest operations to finish entering
                 // split screen.
                 mStageCoordinator.finishEnterSplitScreen(finishT);
                 mPendingRemoteHandler = null;
             }
 
-            mPendingEnter.mCallback.onTransitionConsumed(aborted);
+            mPendingEnter.onConsumed(aborted);
             mPendingEnter = null;
             mPendingRemoteHandler = null;
         } else if (isPendingDismiss(transition)) {
-            mPendingDismiss.mCallback.onTransitionConsumed(aborted);
+            mPendingDismiss.onConsumed(aborted);
             mPendingDismiss = null;
         } else if (isPendingRecent(transition)) {
-            mPendingRecent.mCallback.onTransitionConsumed(aborted);
+            mPendingRecent.onConsumed(aborted);
             mPendingRecent = null;
             mPendingRemoteHandler = null;
         }
@@ -312,23 +330,16 @@
     void onFinish(WindowContainerTransaction wct, WindowContainerTransactionCallback wctCB) {
         if (!mAnimations.isEmpty()) return;
 
-        TransitionCallback callback = null;
+        if (wct == null) wct = new WindowContainerTransaction();
         if (isPendingEnter(mAnimatingTransition)) {
-            callback = mPendingEnter.mCallback;
+            mPendingEnter.onFinished(wct, mFinishTransaction);
             mPendingEnter = null;
-        }
-        if (isPendingDismiss(mAnimatingTransition)) {
-            callback = mPendingDismiss.mCallback;
-            mPendingDismiss = null;
-        }
-        if (isPendingRecent(mAnimatingTransition)) {
-            callback = mPendingRecent.mCallback;
+        } else if (isPendingRecent(mAnimatingTransition)) {
+            mPendingRecent.onFinished(wct, mFinishTransaction);
             mPendingRecent = null;
-        }
-
-        if (callback != null) {
-            if (wct == null) wct = new WindowContainerTransaction();
-            callback.onTransitionFinished(wct, mFinishTransaction);
+        } else if (isPendingDismiss(mAnimatingTransition)) {
+            mPendingDismiss.onFinished(wct, mFinishTransaction);
+            mPendingDismiss = null;
         }
 
         mPendingRemoteHandler = null;
@@ -363,10 +374,7 @@
                 onFinish(null /* wct */, null /* wctCB */);
             });
         };
-        va.addListener(new Animator.AnimatorListener() {
-            @Override
-            public void onAnimationStart(Animator animation) { }
-
+        va.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
                 finisher.run();
@@ -376,9 +384,6 @@
             public void onAnimationCancel(Animator animation) {
                 finisher.run();
             }
-
-            @Override
-            public void onAnimationRepeat(Animator animation) { }
         });
         mAnimations.add(va);
         mTransitions.getAnimExecutor().execute(va::start);
@@ -432,24 +437,66 @@
                 || info.getType() == TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
     }
 
-    /** Clean-up callbacks for transition. */
-    interface TransitionCallback {
-        /** Calls when the transition got consumed. */
-        default void onTransitionConsumed(boolean aborted) {}
+    /** Calls when the transition got consumed. */
+    interface TransitionConsumedCallback {
+        void onConsumed(boolean aborted);
+    }
 
-        /** Calls when the transition finished. */
-        default void onTransitionFinished(WindowContainerTransaction finishWct,
-                SurfaceControl.Transaction finishT) {}
+    /** Calls when the transition finished. */
+    interface TransitionFinishedCallback {
+        void onFinished(WindowContainerTransaction wct, SurfaceControl.Transaction t);
     }
 
     /** Session for a transition and its clean-up callback. */
     static class TransitSession {
         final IBinder mTransition;
-        TransitionCallback mCallback;
+        TransitionConsumedCallback mConsumedCallback;
+        TransitionFinishedCallback mFinishedCallback;
 
-        TransitSession(IBinder transition, @Nullable TransitionCallback callback) {
+        /** Whether the transition was canceled. */
+        boolean mCanceled;
+
+        TransitSession(IBinder transition,
+                @Nullable TransitionConsumedCallback consumedCallback,
+                @Nullable TransitionFinishedCallback finishedCallback) {
             mTransition = transition;
-            mCallback = callback != null ? callback : new TransitionCallback() {};
+            mConsumedCallback = consumedCallback;
+            mFinishedCallback = finishedCallback;
+
+        }
+
+        /** Sets transition consumed callback. */
+        void setConsumedCallback(@Nullable TransitionConsumedCallback callback) {
+            mConsumedCallback = callback;
+        }
+
+        /** Sets transition finished callback. */
+        void setFinishedCallback(@Nullable TransitionFinishedCallback callback) {
+            mFinishedCallback = callback;
+        }
+
+        /**
+         * Cancels the transition. This should be called before playing animation. A canceled
+         * transition will skip playing animation.
+         *
+         * @param finishedCb new finish callback to override.
+         */
+        void cancel(@Nullable TransitionFinishedCallback finishedCb) {
+            mCanceled = true;
+            setFinishedCallback(finishedCb);
+        }
+
+        void onConsumed(boolean aborted) {
+            if (mConsumedCallback != null) {
+                mConsumedCallback.onConsumed(aborted);
+            }
+        }
+
+        void onFinished(WindowContainerTransaction finishWct,
+                SurfaceControl.Transaction finishT) {
+            if (mFinishedCallback != null) {
+                mFinishedCallback.onFinished(finishWct, finishT);
+            }
         }
     }
 
@@ -459,7 +506,7 @@
         final @SplitScreen.StageType int mDismissTop;
 
         DismissTransition(IBinder transition, int reason, int dismissTop) {
-            super(transition, null /* callback */);
+            super(transition, null /* consumedCallback */, null /* finishedCallback */);
             this.mReason = reason;
             this.mDismissTop = dismissTop;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index e2ac01f..943419b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -226,33 +226,36 @@
                 }
             };
 
-    private final SplitScreenTransitions.TransitionCallback mRecentTransitionCallback =
-            new SplitScreenTransitions.TransitionCallback() {
-        @Override
-        public void onTransitionFinished(WindowContainerTransaction finishWct,
-                SurfaceControl.Transaction finishT) {
-            // Check if the recent transition is finished by returning to the current split, so we
-            // can restore the divider bar.
-            for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) {
-                final WindowContainerTransaction.HierarchyOp op =
-                        finishWct.getHierarchyOps().get(i);
-                final IBinder container = op.getContainer();
-                if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop()
-                        && (mMainStage.containsContainer(container)
-                        || mSideStage.containsContainer(container))) {
-                    updateSurfaceBounds(mSplitLayout, finishT, false /* applyResizingOffset */);
-                    setDividerVisibility(true, finishT);
-                    return;
-                }
-            }
+    private final SplitScreenTransitions.TransitionFinishedCallback
+            mRecentTransitionFinishedCallback =
+            new SplitScreenTransitions.TransitionFinishedCallback() {
+                @Override
+                public void onFinished(WindowContainerTransaction finishWct,
+                        SurfaceControl.Transaction finishT) {
+                    // Check if the recent transition is finished by returning to the current
+                    // split, so we
+                    // can restore the divider bar.
+                    for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) {
+                        final WindowContainerTransaction.HierarchyOp op =
+                                finishWct.getHierarchyOps().get(i);
+                        final IBinder container = op.getContainer();
+                        if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop()
+                                && (mMainStage.containsContainer(container)
+                                || mSideStage.containsContainer(container))) {
+                            updateSurfaceBounds(mSplitLayout, finishT,
+                                    false /* applyResizingOffset */);
+                            setDividerVisibility(true, finishT);
+                            return;
+                        }
+                    }
 
-            // Dismiss the split screen if it's not returning to split.
-            prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, finishWct);
-            setSplitsVisible(false);
-            setDividerVisibility(false, finishT);
-            logExit(EXIT_REASON_UNKNOWN);
-        }
-    };
+                    // Dismiss the split screen if it's not returning to split.
+                    prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, finishWct);
+                    setSplitsVisible(false);
+                    setDividerVisibility(false, finishT);
+                    logExit(EXIT_REASON_UNKNOWN);
+                }
+            };
 
     protected StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
             ShellTaskOrganizer taskOrganizer, DisplayController displayController,
@@ -389,15 +392,11 @@
         if (ENABLE_SHELL_TRANSITIONS) {
             prepareEnterSplitScreen(wct);
             mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct,
-                    null, this, new SplitScreenTransitions.TransitionCallback() {
-                        @Override
-                        public void onTransitionFinished(WindowContainerTransaction finishWct,
-                                SurfaceControl.Transaction finishT) {
-                            if (!evictWct.isEmpty()) {
-                                finishWct.merge(evictWct, true);
-                            }
+                    null, this, null /* consumedCallback */, (finishWct, finishT) -> {
+                        if (!evictWct.isEmpty()) {
+                            finishWct.merge(evictWct, true);
                         }
-                    });
+                    } /* finishedCallback */);
         } else {
             if (!evictWct.isEmpty()) {
                 wct.merge(evictWct, true /* transfer */);
@@ -434,28 +433,25 @@
 
         options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */);
         wct.sendPendingIntent(intent, fillInIntent, options);
+
+        // If split screen is not activated, we're expecting to open a pair of apps to split.
+        final int transitType = mMainStage.isActive()
+                ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
         prepareEnterSplitScreen(wct, null /* taskInfo */, position);
 
-        mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, null, this,
-                new SplitScreenTransitions.TransitionCallback() {
-                    @Override
-                    public void onTransitionConsumed(boolean aborted) {
-                        // Switch the split position if launching as MULTIPLE_TASK failed.
-                        if (aborted
-                                && (fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
-                            setSideStagePositionAnimated(
-                                    SplitLayout.reversePosition(mSideStagePosition));
-                        }
+        mSplitTransitions.startEnterTransition(transitType, wct, null, this,
+                aborted -> {
+                    // Switch the split position if launching as MULTIPLE_TASK failed.
+                    if (aborted && (fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
+                        setSideStagePositionAnimated(
+                                SplitLayout.reversePosition(mSideStagePosition));
                     }
-
-                    @Override
-                    public void onTransitionFinished(WindowContainerTransaction finishWct,
-                            SurfaceControl.Transaction finishT) {
-                        if (!evictWct.isEmpty()) {
-                            finishWct.merge(evictWct, true);
-                        }
+                } /* consumedCallback */,
+                (finishWct, finishT) -> {
+                    if (!evictWct.isEmpty()) {
+                        finishWct.merge(evictWct, true);
                     }
-                });
+                } /* finishedCallback */);
     }
 
     /** Launches an activity into split by legacy transition. */
@@ -564,9 +560,9 @@
     /**
      * Starts with the second task to a split pair in one transition.
      *
-     * @param wct transaction to start the first task
+     * @param wct        transaction to start the first task
      * @param instanceId if {@code null}, will not log. Otherwise it will be used in
-     *      {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)}
+     *                   {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)}
      */
     private void startWithTask(WindowContainerTransaction wct, int mainTaskId,
             @Nullable Bundle mainOptions, float splitRatio,
@@ -592,7 +588,7 @@
         wct.startTask(mainTaskId, mainOptions);
 
         mSplitTransitions.startEnterTransition(
-                TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this, null);
+                TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this, null, null);
         setEnterInstanceId(instanceId);
     }
 
@@ -639,7 +635,7 @@
     }
 
     /**
-     * @param wct transaction to start the first task
+     * @param wct        transaction to start the first task
      * @param instanceId if {@code null}, will not log. Otherwise it will be used in
      *                   {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)}
      */
@@ -1082,15 +1078,15 @@
         switch (exitReason) {
             // One of the apps doesn't support MW
             case EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW:
-            // User has explicitly dragged the divider to dismiss split
+                // User has explicitly dragged the divider to dismiss split
             case EXIT_REASON_DRAG_DIVIDER:
-            // Either of the split apps have finished
+                // Either of the split apps have finished
             case EXIT_REASON_APP_FINISHED:
-            // One of the children enters PiP
+                // One of the children enters PiP
             case EXIT_REASON_CHILD_TASK_ENTER_PIP:
-            // One of the apps occludes lock screen.
+                // One of the apps occludes lock screen.
             case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP:
-            // User has unlocked the device after folded
+                // User has unlocked the device after folded
             case EXIT_REASON_DEVICE_FOLDED:
                 return true;
             default:
@@ -1839,7 +1835,7 @@
                         || activityType == ACTIVITY_TYPE_RECENTS) {
                     // Enter overview panel, so start recent transition.
                     mSplitTransitions.setRecentTransition(transition, request.getRemoteTransition(),
-                            mRecentTransitionCallback);
+                            mRecentTransitionFinishedCallback);
                 } else if (mSplitTransitions.mPendingRecent == null) {
                     // If split-task is not controlled by recents animation
                     // and occluded by the other fullscreen task, dismiss both.
@@ -1853,8 +1849,8 @@
                 // One task is appearing into split, prepare to enter split screen.
                 out = new WindowContainerTransaction();
                 prepareEnterSplitScreen(out);
-                mSplitTransitions.setEnterTransition(
-                        transition, request.getRemoteTransition(), null /* callback */);
+                mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(),
+                        null /* consumedCallback */, null /* finishedCallback */);
             }
         }
         return out;
@@ -1873,7 +1869,7 @@
         }
         final @WindowManager.TransitionType int type = request.getType();
         if (isSplitActive() && !isOpeningType(type)
-                    && (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0)) {
+                && (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0)) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  One of the splits became "
                             + "empty during a mixed transition (one not handled by split),"
                             + " so make sure split-screen state is cleaned-up. "
@@ -2031,17 +2027,21 @@
             }
         }
 
-        // TODO(b/250853925): fallback logic. Probably start a new transition to exit split before
-        //       applying anything here. Ideally consolidate with transition-merging.
         if (info.getType() == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) {
             if (mainChild == null && sideChild == null) {
-                throw new IllegalStateException("Launched a task in split, but didn't receive any"
-                        + " task in transition.");
+                Log.w(TAG, "Launched a task in split, but didn't receive any task in transition.");
+                mSplitTransitions.mPendingEnter.cancel(null /* finishedCb */);
+                return true;
             }
         } else {
             if (mainChild == null || sideChild == null) {
-                throw new IllegalStateException("Launched 2 tasks in split, but didn't receive"
+                Log.w(TAG, "Launched 2 tasks in split, but didn't receive"
                         + " 2 tasks in transition. Possibly one of them failed to launch");
+                final int dismissTop = mainChild != null ? STAGE_TYPE_MAIN :
+                        (sideChild != null ? STAGE_TYPE_SIDE : STAGE_TYPE_UNDEFINED);
+                mSplitTransitions.mPendingEnter.cancel(
+                        (cancelWct, cancelT) -> prepareExitSplitScreen(dismissTop, cancelWct));
+                return true;
             }
         }
 
@@ -2305,7 +2305,7 @@
                 final int stageType = isMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
                 final WindowContainerTransaction wct = new WindowContainerTransaction();
                 prepareExitSplitScreen(stageType, wct);
-                mSplitTransitions.startDismissTransition(wct,StageCoordinator.this, stageType,
+                mSplitTransitions.startDismissTransition(wct, StageCoordinator.this, stageType,
                         EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW);
                 mSplitUnsupportedToast.show();
             }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 394d6f6..63d31cd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -122,6 +122,8 @@
     private final ShellController mShellController;
     private final ShellTransitionImpl mImpl = new ShellTransitionImpl();
 
+    private boolean mIsRegistered = false;
+
     /** List of possible handlers. Ordered by specificity (eg. tapped back to front). */
     private final ArrayList<TransitionHandler> mHandlers = new ArrayList<>();
 
@@ -163,19 +165,18 @@
                 displayController, pool, mainExecutor, mainHandler, animExecutor);
         mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor);
         mShellController = shellController;
-        shellInit.addInitCallback(this::onInit, this);
-    }
-
-    private void onInit() {
-        mShellController.addExternalInterface(KEY_EXTRA_SHELL_SHELL_TRANSITIONS,
-                this::createExternalInterface, this);
-
         // The very last handler (0 in the list) should be the default one.
         mHandlers.add(mDefaultTransitionHandler);
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Default");
         // Next lowest priority is remote transitions.
         mHandlers.add(mRemoteTransitionHandler);
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote");
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    private void onInit() {
+        mShellController.addExternalInterface(KEY_EXTRA_SHELL_SHELL_TRANSITIONS,
+                this::createExternalInterface, this);
 
         ContentResolver resolver = mContext.getContentResolver();
         mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
@@ -186,13 +187,23 @@
                 new SettingsObserver());
 
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            mIsRegistered = true;
             // Register this transition handler with Core
-            mOrganizer.registerTransitionPlayer(mPlayerImpl);
+            try {
+                mOrganizer.registerTransitionPlayer(mPlayerImpl);
+            } catch (RuntimeException e) {
+                mIsRegistered = false;
+                throw e;
+            }
             // Pre-load the instance.
             TransitionMetrics.getInstance();
         }
     }
 
+    public boolean isRegistered() {
+        return mIsRegistered;
+    }
+
     private float getTransitionAnimationScaleSetting() {
         return fixScale(Settings.Global.getFloat(mContext.getContentResolver(),
                 Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index ea0033b..652f9b3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -181,7 +181,7 @@
 
         IBinder transition = mSplitScreenTransitions.startEnterTransition(
                 TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(),
-                new RemoteTransition(testRemote), mStageCoordinator, null);
+                new RemoteTransition(testRemote), mStageCoordinator, null, null);
         mMainStage.onTaskAppeared(mMainChild, createMockSurface());
         mSideStage.onTaskAppeared(mSideChild, createMockSurface());
         boolean accepted = mStageCoordinator.startAnimation(transition, info,
@@ -421,7 +421,7 @@
         TransitionInfo enterInfo = createEnterPairInfo();
         IBinder enterTransit = mSplitScreenTransitions.startEnterTransition(
                 TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(),
-                new RemoteTransition(new TestRemoteTransition()), mStageCoordinator, null);
+                new RemoteTransition(new TestRemoteTransition()), mStageCoordinator, null, null);
         mMainStage.onTaskAppeared(mMainChild, createMockSurface());
         mSideStage.onTaskAppeared(mSideChild, createMockSurface());
         mStageCoordinator.startAnimation(enterTransit, enterInfo,
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index 6a0c5a8..d09bc47 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -472,11 +472,11 @@
     mRenderThread.pushBackFrameCallback(this);
 }
 
-nsecs_t CanvasContext::draw() {
+std::optional<nsecs_t> CanvasContext::draw() {
     if (auto grContext = getGrContext()) {
         if (grContext->abandoned()) {
             LOG_ALWAYS_FATAL("GrContext is abandoned/device lost at start of CanvasContext::draw");
-            return 0;
+            return std::nullopt;
         }
     }
     SkRect dirty;
@@ -498,7 +498,7 @@
             std::invoke(func, false /* didProduceBuffer */);
         }
         mFrameCommitCallbacks.clear();
-        return 0;
+        return std::nullopt;
     }
 
     ScopedActiveContext activeContext(this);
@@ -543,6 +543,8 @@
     }
 
     bool requireSwap = false;
+    bool didDraw = false;
+
     int error = OK;
     bool didSwap = mRenderPipeline->swapBuffers(frame, drawResult.success, windowDirty,
                                                 mCurrentFrameInfo, &requireSwap);
@@ -553,7 +555,7 @@
     mIsDirty = false;
 
     if (requireSwap) {
-        bool didDraw = true;
+        didDraw = true;
         // Handle any swapchain errors
         error = mNativeSurface->getAndClearError();
         if (error == TIMED_OUT) {
@@ -649,7 +651,9 @@
     }
 
     mRenderThread.cacheManager().onFrameCompleted();
-    return mCurrentFrameInfo->get(FrameInfoIndex::DequeueBufferDuration);
+    return didDraw ? std::make_optional(
+                             mCurrentFrameInfo->get(FrameInfoIndex::DequeueBufferDuration))
+                   : std::nullopt;
 }
 
 void CanvasContext::reportMetricsWithPresentTime() {
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index 748ab96..db96cfb 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -138,7 +138,7 @@
     bool makeCurrent();
     void prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t syncQueued, RenderNode* target);
     // Returns the DequeueBufferDuration.
-    nsecs_t draw();
+    std::optional<nsecs_t> draw();
     void destroy();
 
     // IFrameCallback, Choreographer-driven frame callback entry point
diff --git a/libs/hwui/renderthread/DrawFrameTask.cpp b/libs/hwui/renderthread/DrawFrameTask.cpp
index 03f02de..dc7676c 100644
--- a/libs/hwui/renderthread/DrawFrameTask.cpp
+++ b/libs/hwui/renderthread/DrawFrameTask.cpp
@@ -19,6 +19,7 @@
 #include <dlfcn.h>
 #include <gui/TraceUtils.h>
 #include <utils/Log.h>
+
 #include <algorithm>
 
 #include "../DeferredLayerUpdater.h"
@@ -28,6 +29,7 @@
 #include "CanvasContext.h"
 #include "RenderThread.h"
 #include "thread/CommonPool.h"
+#include "utils/TimeUtils.h"
 
 namespace android {
 namespace uirenderer {
@@ -146,6 +148,7 @@
 
     bool canUnblockUiThread;
     bool canDrawThisFrame;
+    bool didDraw = false;
     {
         TreeInfo info(TreeInfo::MODE_FULL, *mContext);
         info.forceDrawFrame = mForceDrawFrame;
@@ -188,7 +191,9 @@
 
     nsecs_t dequeueBufferDuration = 0;
     if (CC_LIKELY(canDrawThisFrame)) {
-        dequeueBufferDuration = context->draw();
+        std::optional<nsecs_t> drawResult = context->draw();
+        didDraw = drawResult.has_value();
+        dequeueBufferDuration = drawResult.value_or(0);
     } else {
         // Do a flush in case syncFrameState performed any texture uploads. Since we skipped
         // the draw() call, those uploads (or deletes) will end up sitting in the queue.
@@ -209,8 +214,9 @@
     }
 
     if (!mHintSessionWrapper) mHintSessionWrapper.emplace(mUiThreadId, mRenderThreadId);
-    constexpr int64_t kSanityCheckLowerBound = 100000;       // 0.1ms
-    constexpr int64_t kSanityCheckUpperBound = 10000000000;  // 10s
+
+    constexpr int64_t kSanityCheckLowerBound = 100_us;
+    constexpr int64_t kSanityCheckUpperBound = 10_s;
     int64_t targetWorkDuration = frameDeadline - intendedVsync;
     targetWorkDuration = targetWorkDuration * Properties::targetCpuTimePercentage / 100;
     if (targetWorkDuration > kSanityCheckLowerBound &&
@@ -219,12 +225,15 @@
         mLastTargetWorkDuration = targetWorkDuration;
         mHintSessionWrapper->updateTargetWorkDuration(targetWorkDuration);
     }
-    int64_t frameDuration = systemTime(SYSTEM_TIME_MONOTONIC) - frameStartTime;
-    int64_t actualDuration = frameDuration -
-                             (std::min(syncDelayDuration, mLastDequeueBufferDuration)) -
-                             dequeueBufferDuration;
-    if (actualDuration > kSanityCheckLowerBound && actualDuration < kSanityCheckUpperBound) {
-        mHintSessionWrapper->reportActualWorkDuration(actualDuration);
+
+    if (didDraw) {
+        int64_t frameDuration = systemTime(SYSTEM_TIME_MONOTONIC) - frameStartTime;
+        int64_t actualDuration = frameDuration -
+                                 (std::min(syncDelayDuration, mLastDequeueBufferDuration)) -
+                                 dequeueBufferDuration;
+        if (actualDuration > kSanityCheckLowerBound && actualDuration < kSanityCheckUpperBound) {
+            mHintSessionWrapper->reportActualWorkDuration(actualDuration);
+        }
     }
 
     mLastDequeueBufferDuration = dequeueBufferDuration;
diff --git a/location/java/android/location/GnssCapabilities.java b/location/java/android/location/GnssCapabilities.java
index 7a412a0..b38f9ea 100644
--- a/location/java/android/location/GnssCapabilities.java
+++ b/location/java/android/location/GnssCapabilities.java
@@ -207,7 +207,7 @@
     /**
      * Returns {@code true} if GNSS chipset supports single shot locating, {@code false} otherwise.
      */
-    public boolean hasSingleShot() {
+    public boolean hasSingleShotFix() {
         return (mTopFlags & TOP_HAL_CAPABILITY_SINGLE_SHOT) != 0;
     }
 
@@ -482,7 +482,7 @@
         if (hasMsa()) {
             builder.append("MSA ");
         }
-        if (hasSingleShot()) {
+        if (hasSingleShotFix()) {
             builder.append("SINGLE_SHOT ");
         }
         if (hasOnDemandTime()) {
@@ -602,7 +602,7 @@
         /**
          * Sets single shot locating capability.
          */
-        public @NonNull Builder setHasSingleShot(boolean capable) {
+        public @NonNull Builder setHasSingleShotFix(boolean capable) {
             mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_SINGLE_SHOT, capable);
             return this;
         }
diff --git a/location/java/android/location/GnssStatus.java b/location/java/android/location/GnssStatus.java
index 23390fc..09f40e8 100644
--- a/location/java/android/location/GnssStatus.java
+++ b/location/java/android/location/GnssStatus.java
@@ -199,15 +199,15 @@
      * <li>93-106 as the frequency channel number (FCN) (-7 to +6) plus 100.
      * i.e. encode FCN of -7 as 93, 0 as 100, and +6 as 106</li>
      * </ul></li>
-     * <li>QZSS: 193-200</li>
+     * <li>QZSS: 183-206</li>
      * <li>Galileo: 1-36</li>
-     * <li>Beidou: 1-37</li>
+     * <li>Beidou: 1-63</li>
      * <li>IRNSS: 1-14</li>
      * </ul>
      *
      * @param satelliteIndex An index from zero to {@link #getSatelliteCount()} - 1
      */
-    @IntRange(from = 1, to = 200)
+    @IntRange(from = 1, to = 206)
     public int getSvid(@IntRange(from = 0) int satelliteIndex) {
         return mSvidWithFlags[satelliteIndex] >> SVID_SHIFT_WIDTH;
     }
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java
index 24cb5f9..602e31c 100644
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java
@@ -85,7 +85,7 @@
 
         SliceStoreBroadcastReceiver.updateSliceStoreActivity(mCapability, this);
 
-        mWebView = new WebView(getApplicationContext());
+        mWebView = new WebView(this);
         setContentView(mWebView);
         mWebView.loadUrl(mUrl.toString());
         // TODO(b/245882601): Get back response from WebView
diff --git a/packages/CompanionDeviceManager/res/values/styles.xml b/packages/CompanionDeviceManager/res/values/styles.xml
index 428f2dc..2000d96 100644
--- a/packages/CompanionDeviceManager/res/values/styles.xml
+++ b/packages/CompanionDeviceManager/res/values/styles.xml
@@ -49,7 +49,6 @@
     <style name="DescriptionSummary">
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">wrap_content</item>
-        <item name="android:gravity">center</item>
         <item name="android:layout_marginTop">18dp</item>
         <item name="android:layout_marginLeft">18dp</item>
         <item name="android:layout_marginRight">18dp</item>
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index e037db7..2ba8748 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -35,7 +35,7 @@
         ProviderInfo(
           // TODO: replace to extract from the service data structure when available
           icon = context.getDrawable(R.drawable.ic_passkey)!!,
-          name = it.providerId,
+          name = it.providerFlattenedComponentName,
           displayName = it.providerDisplayName,
           credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
           credentialOptions = toCredentialOptionInfoList(it.credentialEntries, context)
@@ -79,7 +79,7 @@
         com.android.credentialmanager.createflow.ProviderInfo(
           // TODO: replace to extract from the service data structure when available
           icon = context.getDrawable(R.drawable.ic_passkey)!!,
-          name = it.providerId,
+          name = it.providerFlattenedComponentName,
           displayName = it.providerDisplayName,
           credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
           createOptions = toCreationOptionInfoList(it.credentialEntries, context),
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle
index 2820ed7..3b159e9 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle
+++ b/packages/SettingsLib/Spa/spa/build.gradle
@@ -59,9 +59,9 @@
     api "androidx.compose.material:material-icons-extended:$jetpack_compose_version"
     api "androidx.compose.runtime:runtime-livedata:$jetpack_compose_version"
     api "androidx.compose.ui:ui-tooling-preview:$jetpack_compose_version"
-    api "androidx.lifecycle:lifecycle-livedata-ktx:2.6.0-alpha02"
+    api "androidx.lifecycle:lifecycle-livedata-ktx:2.6.0-alpha03"
     api "androidx.navigation:navigation-compose:2.5.0"
-    api "com.google.android.material:material:1.6.1"
+    api "com.google.android.material:material:1.7.0-alpha03"
     debugApi "androidx.compose.ui:ui-tooling:$jetpack_compose_version"
     implementation "com.airbnb.android:lottie-compose:5.2.0"
 }
diff --git a/packages/SettingsLib/Spa/spa/res/values-night/themes.xml b/packages/SettingsLib/Spa/spa/res/values-night/themes.xml
deleted file mode 100644
index 67dd2b0..0000000
--- a/packages/SettingsLib/Spa/spa/res/values-night/themes.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  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.
--->
-<resources>
-
-    <style name="Theme.SpaLib.DayNight" />
-</resources>
diff --git a/packages/SettingsLib/Spa/spa/res/values/themes.xml b/packages/SettingsLib/Spa/spa/res/values/themes.xml
index e0e5fc2..25846ec 100644
--- a/packages/SettingsLib/Spa/spa/res/values/themes.xml
+++ b/packages/SettingsLib/Spa/spa/res/values/themes.xml
@@ -16,12 +16,10 @@
 -->
 <resources>
 
-    <style name="Theme.SpaLib" parent="Theme.Material3.DayNight.NoActionBar">
+    <style name="Theme.SpaLib" parent="@android:style/Theme.DeviceDefault.Settings">
         <item name="android:statusBarColor">@android:color/transparent</item>
         <item name="android:navigationBarColor">@android:color/transparent</item>
-    </style>
-
-    <style name="Theme.SpaLib.DayNight">
-        <item name="android:windowLightStatusBar">true</item>
+        <item name="android:windowActionBar">false</item>
+        <item name="android:windowNoTitle">true</item>
     </style>
 </resources>
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
index d3efaa7..c3c90ab 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
@@ -27,6 +27,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.core.view.WindowCompat
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
 import androidx.navigation.NavGraph.Companion.findStartDestination
@@ -66,8 +67,9 @@
     private val spaEnvironment get() = SpaEnvironmentFactory.instance
 
     override fun onCreate(savedInstanceState: Bundle?) {
-        setTheme(R.style.Theme_SpaLib_DayNight)
+        setTheme(R.style.Theme_SpaLib)
         super.onCreate(savedInstanceState)
+        WindowCompat.setDecorFitsSystemWindows(window, false)
         spaEnvironment.logger.message(TAG, "onCreate", category = LogCategory.FRAMEWORK)
 
         setContent {
@@ -83,35 +85,19 @@
         val navController = rememberNavController()
         val nullPage = SettingsPage.createNull()
         CompositionLocalProvider(navController.localNavController()) {
-            NavHost(navController, nullPage.sppName) {
+            NavHost(
+                navController = navController,
+                startDestination = nullPage.sppName,
+            ) {
                 composable(nullPage.sppName) {}
                 for (spp in sppRepository.getAllProviders()) {
                     composable(
                         route = spp.name + spp.parameter.navRoute(),
                         arguments = spp.parameter,
                     ) { navBackStackEntry ->
-                        val lifecycleOwner = LocalLifecycleOwner.current
-                        val sp = remember(navBackStackEntry.arguments) {
+                        PageLogger(remember(navBackStackEntry.arguments) {
                             spp.createSettingsPage(arguments = navBackStackEntry.arguments)
-                        }
-
-                        DisposableEffect(lifecycleOwner) {
-                            val observer = LifecycleEventObserver { _, event ->
-                                if (event == Lifecycle.Event.ON_START) {
-                                    sp.enterPage()
-                                } else if (event == Lifecycle.Event.ON_STOP) {
-                                    sp.leavePage()
-                                }
-                            }
-
-                            // Add the observer to the lifecycle
-                            lifecycleOwner.lifecycle.addObserver(observer)
-
-                            // When the effect leaves the Composition, remove the observer
-                            onDispose {
-                                lifecycleOwner.lifecycle.removeObserver(observer)
-                            }
-                        }
+                        })
 
                         spp.Page(navBackStackEntry.arguments)
                     }
@@ -122,6 +108,28 @@
     }
 
     @Composable
+    private fun PageLogger(settingsPage: SettingsPage) {
+        val lifecycleOwner = LocalLifecycleOwner.current
+        DisposableEffect(lifecycleOwner) {
+            val observer = LifecycleEventObserver { _, event ->
+                if (event == Lifecycle.Event.ON_START) {
+                    settingsPage.enterPage()
+                } else if (event == Lifecycle.Event.ON_STOP) {
+                    settingsPage.leavePage()
+                }
+            }
+
+            // Add the observer to the lifecycle
+            lifecycleOwner.lifecycle.addObserver(observer)
+
+            // When the effect leaves the Composition, remove the observer
+            onDispose {
+                lifecycleOwner.lifecycle.removeObserver(observer)
+            }
+        }
+    }
+
+    @Composable
     private fun InitialDestinationNavigator() {
         val sppRepository by spaEnvironment.pageProviderRepository
         val destinationNavigated = rememberSaveable { mutableStateOf(false) }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/PaddingValuesExt.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/PaddingValuesExt.kt
new file mode 100644
index 0000000..18335ff
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/PaddingValuesExt.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.compose
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+
+internal fun PaddingValues.horizontalValues(): PaddingValues = HorizontalPaddingValues(this)
+
+internal fun PaddingValues.verticalValues(): PaddingValues = VerticalPaddingValues(this)
+
+private class HorizontalPaddingValues(private val paddingValues: PaddingValues) : PaddingValues {
+    override fun calculateLeftPadding(layoutDirection: LayoutDirection) =
+        paddingValues.calculateLeftPadding(layoutDirection)
+
+    override fun calculateTopPadding(): Dp = 0.dp
+
+    override fun calculateRightPadding(layoutDirection: LayoutDirection) =
+        paddingValues.calculateRightPadding(layoutDirection)
+
+    override fun calculateBottomPadding() = 0.dp
+}
+
+private class VerticalPaddingValues(private val paddingValues: PaddingValues) : PaddingValues {
+    override fun calculateLeftPadding(layoutDirection: LayoutDirection) = 0.dp
+
+    override fun calculateTopPadding(): Dp = paddingValues.calculateTopPadding()
+
+    override fun calculateRightPadding(layoutDirection: LayoutDirection) = 0.dp
+
+    override fun calculateBottomPadding() = paddingValues.calculateBottomPadding()
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
index 9eaa88a..26491d5 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
@@ -62,7 +62,7 @@
     private val spaEnvironment get() = SpaEnvironmentFactory.instance
 
     override fun onCreate(savedInstanceState: Bundle?) {
-        setTheme(R.style.Theme_SpaLib_DayNight)
+        setTheme(R.style.Theme_SpaLib)
         super.onCreate(savedInstanceState)
         spaEnvironment.logger.message(TAG, "onCreate", category = LogCategory.FRAMEWORK)
 
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt
index eb20ac5..711c8a7 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBarsPadding
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.MaterialTheme
@@ -34,6 +35,7 @@
         Modifier
             .fillMaxSize()
             .background(color = MaterialTheme.colorScheme.background)
+            .systemBarsPadding()
             .verticalScroll(rememberScrollState()),
     ) {
         Text(
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/RegularScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/RegularScaffold.kt
index 9a17b2a..d17a8dc 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/RegularScaffold.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/RegularScaffold.kt
@@ -19,7 +19,7 @@
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.Scaffold
@@ -27,6 +27,8 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
 
 /**
  * A [Scaffold] which content is scrollable and wrapped in a [Column].
@@ -42,8 +44,9 @@
 ) {
     SettingsScaffold(title, actions) { paddingValues ->
         Column(Modifier.verticalScroll(rememberScrollState())) {
-            Spacer(Modifier.padding(paddingValues))
+            Spacer(Modifier.height(paddingValues.calculateTopPadding()))
             content()
+            Spacer(Modifier.height(paddingValues.calculateBottomPadding()))
         }
     }
 }
@@ -52,6 +55,13 @@
 @Composable
 private fun RegularScaffoldPreview() {
     SettingsTheme {
-        RegularScaffold(title = "Display") {}
+        RegularScaffold(title = "Display") {
+            Preference(object : PreferenceModel {
+                override val title = "Item 1"
+            })
+            Preference(object : PreferenceModel {
+                override val title = "Item 2"
+            })
+        }
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
index 4f83ad6..efc623a 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
@@ -14,13 +14,12 @@
  * limitations under the License.
  */
 
-@file:OptIn(ExperimentalMaterial3Api::class)
-
 package com.android.settingslib.spa.widget.scaffold
 
 import androidx.activity.compose.BackHandler
 import androidx.appcompat.R
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -31,10 +30,13 @@
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
 import androidx.compose.material3.TextField
 import androidx.compose.material3.TextFieldDefaults
 import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBarScrollBehavior
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.State
@@ -48,45 +50,57 @@
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewmodel.compose.viewModel
 import com.android.settingslib.spa.framework.compose.hideKeyboardAction
+import com.android.settingslib.spa.framework.compose.horizontalValues
 import com.android.settingslib.spa.framework.theme.SettingsOpacity
 import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
 
 /**
  * A [Scaffold] which content is can be full screen, and with a search feature built-in.
  */
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun SearchScaffold(
     title: String,
     actions: @Composable RowScope.() -> Unit = {},
-    content: @Composable (searchQuery: State<String>) -> Unit,
+    content: @Composable (bottomPadding: Dp, searchQuery: State<String>) -> Unit,
 ) {
     val viewModel: SearchScaffoldViewModel = viewModel()
 
+    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
     Scaffold(
+        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         topBar = {
             SearchableTopAppBar(
                 title = title,
                 actions = actions,
+                scrollBehavior = scrollBehavior,
                 searchQuery = viewModel.searchQuery,
             ) { viewModel.searchQuery = it }
         },
     ) { paddingValues ->
         Box(
             Modifier
-                .padding(paddingValues)
-                .fillMaxSize()
+                .padding(paddingValues.horizontalValues())
+                .padding(top = paddingValues.calculateTopPadding())
+                .fillMaxSize(),
         ) {
-            val searchQuery = remember {
-                derivedStateOf { viewModel.searchQuery?.text ?: "" }
-            }
-            content(searchQuery)
+            content(
+                bottomPadding = paddingValues.calculateBottomPadding(),
+                searchQuery = remember {
+                    derivedStateOf { viewModel.searchQuery?.text ?: "" }
+                },
+            )
         }
     }
 }
@@ -95,10 +109,12 @@
     var searchQuery: TextFieldValue? by mutableStateOf(null)
 }
 
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun SearchableTopAppBar(
     title: String,
     actions: @Composable RowScope.() -> Unit,
+    scrollBehavior: TopAppBarScrollBehavior,
     searchQuery: TextFieldValue?,
     onSearchQueryChange: (TextFieldValue?) -> Unit,
 ) {
@@ -110,13 +126,17 @@
             actions = actions,
         )
     } else {
-        SettingsTopAppBar(title) {
-            SearchAction { onSearchQueryChange(TextFieldValue()) }
+        SettingsTopAppBar(title, scrollBehavior) {
+            SearchAction {
+                scrollBehavior.collapse()
+                onSearchQueryChange(TextFieldValue())
+            }
             actions()
         }
     }
 }
 
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun SearchTopAppBar(
     query: TextFieldValue,
@@ -124,21 +144,24 @@
     onClose: () -> Unit,
     actions: @Composable RowScope.() -> Unit = {},
 ) {
-    TopAppBar(
-        title = { SearchBox(query, onQueryChange) },
-        modifier = Modifier.statusBarsPadding(),
-        navigationIcon = { CollapseAction(onClose) },
-        actions = {
-            if (query.text.isNotEmpty()) {
-                ClearAction { onQueryChange(TextFieldValue()) }
-            }
-            actions()
-        },
-        colors = settingsTopAppBarColors(),
-    )
+    Surface(color = SettingsTheme.colorScheme.surfaceHeader) {
+        TopAppBar(
+            title = { SearchBox(query, onQueryChange) },
+            modifier = Modifier.statusBarsPadding(),
+            navigationIcon = { CollapseAction(onClose) },
+            actions = {
+                if (query.text.isNotEmpty()) {
+                    ClearAction { onQueryChange(TextFieldValue()) }
+                }
+                actions()
+            },
+            colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = Color.Transparent),
+        )
+    }
     BackHandler { onClose() }
 }
 
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun SearchBox(query: TextFieldValue, onQueryChange: (TextFieldValue) -> Unit) {
     val focusRequester = remember { FocusRequester() }
@@ -184,6 +207,15 @@
 @Composable
 private fun SearchScaffoldPreview() {
     SettingsTheme {
-        SearchScaffold(title = "App notifications") {}
+        SearchScaffold(title = "App notifications") { _, _ ->
+            Column {
+                Preference(object : PreferenceModel {
+                    override val title = "Item 1"
+                })
+                Preference(object : PreferenceModel {
+                    override val title = "Item 2"
+                })
+            }
+        }
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt
index 3bc3dd7..f4e504a 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt
@@ -16,13 +16,23 @@
 
 package com.android.settingslib.spa.widget.scaffold
 
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Scaffold
+import androidx.compose.material3.TopAppBarDefaults
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.compose.horizontalValues
+import com.android.settingslib.spa.framework.compose.verticalValues
 import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
 
 /**
  * A [Scaffold] which content is can be full screen when needed.
@@ -34,16 +44,30 @@
     actions: @Composable RowScope.() -> Unit = {},
     content: @Composable (PaddingValues) -> Unit,
 ) {
+    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
     Scaffold(
-        topBar = { SettingsTopAppBar(title, actions) },
-        content = content,
-    )
+        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+        topBar = { SettingsTopAppBar(title, scrollBehavior, actions) },
+    ) { paddingValues ->
+        Box(Modifier.padding(paddingValues.horizontalValues())) {
+            content(paddingValues.verticalValues())
+        }
+    }
 }
 
 @Preview
 @Composable
 private fun SettingsScaffoldPreview() {
     SettingsTheme {
-        SettingsScaffold(title = "Display") {}
+        SettingsScaffold(title = "Display") { paddingValues ->
+            Column(Modifier.padding(paddingValues)) {
+                Preference(object : PreferenceModel {
+                    override val title = "Item 1"
+                })
+                Preference(object : PreferenceModel {
+                    override val title = "Item 2"
+                })
+            }
+        }
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt
index 9353520..f7cb035 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt
@@ -17,41 +17,70 @@
 package com.android.settingslib.spa.widget.scaffold
 
 import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.navigationBars
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.LargeTopAppBar
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
 import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBarScrollBehavior
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.text.style.TextOverflow
+import com.android.settingslib.spa.framework.compose.horizontalValues
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.framework.theme.rememberSettingsTypography
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 internal fun SettingsTopAppBar(
     title: String,
+    scrollBehavior: TopAppBarScrollBehavior,
     actions: @Composable RowScope.() -> Unit,
 ) {
-    TopAppBar(
-        title = {
-            Text(
-                text = title,
-                modifier = Modifier.padding(SettingsDimension.itemPaddingAround),
-                overflow = TextOverflow.Ellipsis,
-                maxLines = 1,
-            )
-        },
-        navigationIcon = { NavigateBack() },
-        actions = actions,
-        colors = settingsTopAppBarColors(),
+    val colorScheme = MaterialTheme.colorScheme
+    // TODO: Remove MaterialTheme() after top app bar color fixed in AndroidX.
+    MaterialTheme(
+        colorScheme = remember { colorScheme.copy(surface = colorScheme.background) },
+        typography = rememberSettingsTypography(),
+    ) {
+        LargeTopAppBar(
+            title = { Title(title) },
+            navigationIcon = { NavigateBack() },
+            actions = actions,
+            colors = largeTopAppBarColors(),
+            scrollBehavior = scrollBehavior,
+        )
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+internal fun TopAppBarScrollBehavior.collapse() {
+    with(state) {
+        heightOffset = heightOffsetLimit
+    }
+}
+
+@Composable
+private fun Title(title: String) {
+    Text(
+        text = title,
+        modifier = Modifier
+            .padding(WindowInsets.navigationBars.asPaddingValues().horizontalValues())
+            .padding(SettingsDimension.itemPaddingAround),
+        overflow = TextOverflow.Ellipsis,
+        maxLines = 1,
     )
 }
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
-internal fun settingsTopAppBarColors() = TopAppBarDefaults.smallTopAppBarColors(
-    containerColor = SettingsTheme.colorScheme.surfaceHeader,
+private fun largeTopAppBarColors() = TopAppBarDefaults.largeTopAppBarColors(
+    containerColor = MaterialTheme.colorScheme.background,
     scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader,
 )
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt
index 652e54d..d09aec9 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt
@@ -16,21 +16,42 @@
 
 package com.android.settingslib.spa.widget.util
 
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.repeatable
+import androidx.compose.animation.core.tween
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+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 androidx.compose.ui.graphics.Color
 import com.android.settingslib.spa.framework.common.LocalEntryDataProvider
+import com.android.settingslib.spa.framework.theme.SettingsTheme
 
 @Composable
 internal fun EntryHighlight(UiLayoutFn: @Composable () -> Unit) {
     val entryData = LocalEntryDataProvider.current
-    val isHighlighted = rememberSaveable { entryData.isHighlighted }
-    val backgroundColor =
-        if (isHighlighted) MaterialTheme.colorScheme.surfaceVariant else Color.Transparent
+    var isHighlighted by rememberSaveable { mutableStateOf(false) }
+    SideEffect {
+        isHighlighted = entryData.isHighlighted
+    }
+
+    val backgroundColor by animateColorAsState(
+        targetValue = when {
+            isHighlighted -> MaterialTheme.colorScheme.surfaceVariant
+            else -> SettingsTheme.colorScheme.background
+        },
+        animationSpec = repeatable(
+            iterations = 3,
+            animation = tween(durationMillis = 500),
+            repeatMode = RepeatMode.Restart
+        )
+    )
     Box(modifier = Modifier.background(color = backgroundColor)) {
         UiLayoutFn()
     }
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/RegularScaffoldTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/RegularScaffoldTest.kt
new file mode 100644
index 0000000..1964c43
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/RegularScaffoldTest.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.widget.scaffold
+
+import androidx.compose.material3.Text
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class RegularScaffoldTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun regularScaffold_titleIsDisplayed() {
+        composeTestRule.setContent {
+            RegularScaffold(title = TITLE) {
+                Text(text = "AAA")
+                Text(text = "BBB")
+            }
+        }
+
+        composeTestRule.onNodeWithText(TITLE).assertIsDisplayed()
+    }
+
+    @Test
+    fun regularScaffold_itemsAreDisplayed() {
+        composeTestRule.setContent {
+            RegularScaffold(title = TITLE) {
+                Text(text = "AAA")
+                Text(text = "BBB")
+            }
+        }
+
+        composeTestRule.onNodeWithText("AAA").assertIsDisplayed()
+        composeTestRule.onNodeWithText("BBB").assertIsDisplayed()
+    }
+
+    private companion object {
+        const val TITLE = "title"
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt
index ec3379d..c3e1d54 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt
@@ -43,7 +43,7 @@
     @Test
     fun initialState_titleIsDisplayed() {
         composeTestRule.setContent {
-            SearchScaffold(title = TITLE) {}
+            SearchScaffold(title = TITLE) { _, _ -> }
         }
 
         composeTestRule.onNodeWithText(TITLE).assertIsDisplayed()
@@ -116,7 +116,7 @@
     private fun setContent(): State<String> {
         lateinit var actualSearchQuery: State<String>
         composeTestRule.setContent {
-            SearchScaffold(title = TITLE) { searchQuery ->
+            SearchScaffold(title = TITLE) { _, searchQuery ->
                 SideEffect {
                     actualSearchQuery = searchQuery
                 }
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerKtTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerTest.kt
similarity index 89%
rename from packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerKtTest.kt
rename to packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerTest.kt
index 0c84eac..0c745d5 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerKtTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerTest.kt
@@ -16,7 +16,6 @@
 
 package com.android.settingslib.spa.widget.scaffold
 
-import androidx.compose.runtime.Composable
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsNotDisplayed
 import androidx.compose.ui.test.assertIsNotSelected
@@ -31,15 +30,13 @@
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
-class SettingsPagerKtTest {
+class SettingsPagerTest {
     @get:Rule
     val composeTestRule = createComposeRule()
 
     @Test
     fun twoPage_initialState() {
-        composeTestRule.setContent {
-            TestTwoPage()
-        }
+        setTwoPagesContent()
 
         composeTestRule.onNodeWithText("Personal").assertIsSelected()
         composeTestRule.onNodeWithText("Page 0").assertIsDisplayed()
@@ -49,9 +46,7 @@
 
     @Test
     fun twoPage_afterSwitch() {
-        composeTestRule.setContent {
-            TestTwoPage()
-        }
+        setTwoPagesContent()
 
         composeTestRule.onNodeWithText("Work").performClick()
 
@@ -73,11 +68,12 @@
         composeTestRule.onNodeWithText("Page 0").assertIsDisplayed()
         composeTestRule.onNodeWithText("Page 1").assertDoesNotExist()
     }
-}
 
-@Composable
-private fun TestTwoPage() {
-    SettingsPager(listOf("Personal", "Work")) {
-        SettingsTitle(title = "Page $it")
+    private fun setTwoPagesContent() {
+        composeTestRule.setContent {
+            SettingsPager(listOf("Personal", "Work")) {
+                SettingsTitle(title = "Page $it")
+            }
+        }
     }
 }
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffoldTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffoldTest.kt
new file mode 100644
index 0000000..f042404
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffoldTest.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.widget.scaffold
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.material3.Text
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SettingsScaffoldTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun settingsScaffold_titleIsDisplayed() {
+        composeTestRule.setContent {
+            SettingsScaffold(title = TITLE) {
+                Text(text = "AAA")
+                Text(text = "BBB")
+            }
+        }
+
+        composeTestRule.onNodeWithText(TITLE).assertIsDisplayed()
+    }
+
+    @Test
+    fun settingsScaffold_itemsAreDisplayed() {
+        composeTestRule.setContent {
+            SettingsScaffold(title = TITLE) {
+                Text(text = "AAA")
+                Text(text = "BBB")
+            }
+        }
+
+        composeTestRule.onNodeWithText("AAA").assertIsDisplayed()
+        composeTestRule.onNodeWithText("BBB").assertIsDisplayed()
+    }
+
+    @Test
+    fun settingsScaffold_noHorizontalPadding() {
+        lateinit var actualPaddingValues: PaddingValues
+
+        composeTestRule.setContent {
+            SettingsScaffold(title = TITLE) { paddingValues ->
+                SideEffect {
+                    actualPaddingValues = paddingValues
+                }
+            }
+        }
+
+        assertThat(actualPaddingValues.calculateLeftPadding(LayoutDirection.Ltr)).isEqualTo(0.dp)
+        assertThat(actualPaddingValues.calculateLeftPadding(LayoutDirection.Rtl)).isEqualTo(0.dp)
+        assertThat(actualPaddingValues.calculateRightPadding(LayoutDirection.Ltr)).isEqualTo(0.dp)
+        assertThat(actualPaddingValues.calculateRightPadding(LayoutDirection.Rtl)).isEqualTo(0.dp)
+    }
+
+    private companion object {
+        const val TITLE = "title"
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfos.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfos.kt
index c1ac5d4..8954d22 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfos.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfos.kt
@@ -35,6 +35,9 @@
 /** Checks whether a flag is associated with the application. */
 fun ApplicationInfo.hasFlag(flag: Int): Boolean = (flags and flag) > 0
 
+/** Checks whether the application is currently installed. */
+val ApplicationInfo.installed: Boolean get() = hasFlag(ApplicationInfo.FLAG_INSTALLED)
+
 /** Checks whether the application is disabled until used. */
 val ApplicationInfo.isDisabledUntilUsed: Boolean
     get() = enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
index 408b9df..3cd8378 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
@@ -25,12 +25,12 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.Dp
 import androidx.lifecycle.viewmodel.compose.viewModel
 import com.android.settingslib.spa.framework.compose.LogCompositions
 import com.android.settingslib.spa.framework.compose.TimeMeasurer.Companion.rememberTimeMeasurer
 import com.android.settingslib.spa.framework.compose.rememberLazyListStateAndHideKeyboardWhenStartScroll
 import com.android.settingslib.spa.framework.compose.toState
-import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.widget.ui.PlaceholderTitle
 import com.android.settingslib.spaprivileged.R
 import com.android.settingslib.spaprivileged.model.app.AppListConfig
@@ -55,10 +55,11 @@
     option: State<Int>,
     searchQuery: State<String>,
     appItem: @Composable (itemState: AppListItemModel<T>) -> Unit,
+    bottomPadding: Dp,
 ) {
     LogCompositions(TAG, appListConfig.userId.toString())
     val appListData = loadAppEntries(appListConfig, listModel, showSystem, option, searchQuery)
-    AppListWidget(appListData, listModel, appItem)
+    AppListWidget(appListData, listModel, appItem, bottomPadding)
 }
 
 @Composable
@@ -66,6 +67,7 @@
     appListData: State<AppListData<T>?>,
     listModel: AppListModel<T>,
     appItem: @Composable (itemState: AppListItemModel<T>) -> Unit,
+    bottomPadding: Dp,
 ) {
     val timeMeasurer = rememberTimeMeasurer(TAG)
     appListData.value?.let { (list, option) ->
@@ -77,7 +79,7 @@
         LazyColumn(
             modifier = Modifier.fillMaxSize(),
             state = rememberLazyListStateAndHideKeyboardWhenStartScroll(),
-            contentPadding = PaddingValues(bottom = SettingsDimension.itemPaddingVertical),
+            contentPadding = PaddingValues(bottom = bottomPadding),
         ) {
             items(count = list.size, key = { option to list[it].record.app.packageName }) {
                 val appEntry = list[it]
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
index 99376b0..2953367 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
@@ -52,7 +52,7 @@
         actions = {
             ShowSystemAction(showSystem.value) { showSystem.value = it }
         },
-    ) { searchQuery ->
+    ) { bottomPadding, searchQuery ->
         WorkProfilePager(primaryUserOnly) { userInfo ->
             Column(Modifier.fillMaxSize()) {
                 val options = remember { listModel.getSpinnerOptions() }
@@ -68,6 +68,7 @@
                     option = selectedOption,
                     searchQuery = searchQuery,
                     appItem = appItem,
+                    bottomPadding = bottomPadding,
                 )
             }
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
index 03d9f2d..30d3820 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java
@@ -357,5 +357,12 @@
          * {@link SubscriptionManager#getDefaultSubscriptionId()}.
          */
         public static final String COLUMN_IS_DEFAULT_SUBSCRIPTION = "isDefaultSubscription";
+
+        /**
+         * The name of the active data subscription state column, see
+         * {@link SubscriptionManager#getActiveDataSubscriptionId()}.
+         */
+        public static final String COLUMN_IS_ACTIVE_DATA_SUBSCRIPTION =
+                "isActiveDataSubscriptionId";
     }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java
index 329bd9b..23566f7 100644
--- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java
+++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java
@@ -42,7 +42,7 @@
             boolean isUsableSubscription, boolean isActiveSubscriptionId,
             boolean isAvailableSubscription, boolean isDefaultVoiceSubscription,
             boolean isDefaultSmsSubscription, boolean isDefaultDataSubscription,
-            boolean isDefaultSubscription) {
+            boolean isDefaultSubscription, boolean isActiveDataSubscriptionId) {
         this.subId = subId;
         this.simSlotIndex = simSlotIndex;
         this.carrierId = carrierId;
@@ -72,6 +72,7 @@
         this.isDefaultSmsSubscription = isDefaultSmsSubscription;
         this.isDefaultDataSubscription = isDefaultDataSubscription;
         this.isDefaultSubscription = isDefaultSubscription;
+        this.isActiveDataSubscriptionId = isActiveDataSubscriptionId;
     }
 
     @PrimaryKey
@@ -165,6 +166,9 @@
     @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_DEFAULT_SUBSCRIPTION)
     public boolean isDefaultSubscription;
 
+    @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_ACTIVE_DATA_SUBSCRIPTION)
+    public boolean isActiveDataSubscriptionId;
+
     public int getSubId() {
         return Integer.valueOf(subId);
     }
@@ -213,6 +217,7 @@
         result = 31 * result + Boolean.hashCode(isDefaultSmsSubscription);
         result = 31 * result + Boolean.hashCode(isDefaultDataSubscription);
         result = 31 * result + Boolean.hashCode(isDefaultSubscription);
+        result = 31 * result + Boolean.hashCode(isActiveDataSubscriptionId);
         return result;
     }
 
@@ -254,7 +259,8 @@
                 && isDefaultVoiceSubscription == info.isDefaultVoiceSubscription
                 && isDefaultSmsSubscription == info.isDefaultSmsSubscription
                 && isDefaultDataSubscription == info.isDefaultDataSubscription
-                && isDefaultSubscription == info.isDefaultSubscription;
+                && isDefaultSubscription == info.isDefaultSubscription
+                && isActiveDataSubscriptionId == info.isActiveDataSubscriptionId;
     }
 
     public String toString() {
@@ -317,6 +323,8 @@
                 .append(isDefaultDataSubscription)
                 .append(", isDefaultSubscription = ")
                 .append(isDefaultSubscription)
+                .append(", isActiveDataSubscriptionId = ")
+                .append(isActiveDataSubscriptionId)
                 .append(")}");
         return builder.toString();
     }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index ccbfac2..fa96a2f 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -5533,13 +5533,17 @@
                 }
                 if (currentVersion == 210) {
                     final SettingsState secureSettings = getSecureSettingsLocked(userId);
-                    final int defaultValueVibrateIconEnabled = getContext().getResources()
-                            .getInteger(R.integer.def_statusBarVibrateIconEnabled);
-                    secureSettings.insertSettingOverrideableByRestoreLocked(
-                            Secure.STATUS_BAR_SHOW_VIBRATE_ICON,
-                            String.valueOf(defaultValueVibrateIconEnabled),
-                            null /* tag */, true /* makeDefault */,
-                            SettingsState.SYSTEM_PACKAGE_NAME);
+                    final Setting currentSetting = secureSettings.getSettingLocked(
+                            Secure.STATUS_BAR_SHOW_VIBRATE_ICON);
+                    if (currentSetting.isNull()) {
+                        final int defaultValueVibrateIconEnabled = getContext().getResources()
+                                .getInteger(R.integer.def_statusBarVibrateIconEnabled);
+                        secureSettings.insertSettingOverrideableByRestoreLocked(
+                                Secure.STATUS_BAR_SHOW_VIBRATE_ICON,
+                                String.valueOf(defaultValueVibrateIconEnabled),
+                                null /* tag */, true /* makeDefault */,
+                                SettingsState.SYSTEM_PACKAGE_NAME);
+                    }
                     currentVersion = 211;
                 }
                 // vXXX: Add new settings above this point.
diff --git a/packages/SystemUI/docs/device-entry/quickaffordance.md b/packages/SystemUI/docs/device-entry/quickaffordance.md
index 38d636d7..95b986f 100644
--- a/packages/SystemUI/docs/device-entry/quickaffordance.md
+++ b/packages/SystemUI/docs/device-entry/quickaffordance.md
@@ -8,7 +8,7 @@
 ### Step 1: create a new quick affordance config
 * Create a new class under the [systemui/keyguard/domain/quickaffordance](../../src/com/android/systemui/keyguard/domain/quickaffordance) directory
 * Please make sure that the class is injected through the Dagger dependency injection system by using the `@Inject` annotation on its main constructor and the `@SysUISingleton` annotation at class level, to make sure only one instance of the class is ever instantiated
-* Have the class implement the [KeyguardQuickAffordanceConfig](../../src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt) interface, notes:
+* Have the class implement the [KeyguardQuickAffordanceConfig](../../src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt) interface, notes:
   * The `state` Flow property must emit `State.Hidden` when the feature is not enabled!
   * It is safe to assume that `onQuickAffordanceClicked` will not be invoked if-and-only-if the previous rule is followed
   * When implementing `onQuickAffordanceClicked`, the implementation can do something or it can ask the framework to start an activity using an `Intent` provided by the implementation
diff --git a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml
index 3bcc37a..e2ce34f 100644
--- a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml
+++ b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml
@@ -14,7 +14,7 @@
   ~ limitations under the License.
   -->
 
-<com.android.systemui.biometrics.AuthCredentialPasswordView
+<com.android.systemui.biometrics.ui.CredentialPasswordView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
@@ -86,4 +86,4 @@
 
     </LinearLayout>
 
-</com.android.systemui.biometrics.AuthCredentialPasswordView>
\ No newline at end of file
+</com.android.systemui.biometrics.ui.CredentialPasswordView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
index a3dd334..6e0e38b 100644
--- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
+++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
@@ -14,7 +14,7 @@
   ~ limitations under the License.
   -->
 
-<com.android.systemui.biometrics.AuthCredentialPatternView
+<com.android.systemui.biometrics.ui.CredentialPatternView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
@@ -83,4 +83,4 @@
 
     </FrameLayout>
 
-</com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file
+</com.android.systemui.biometrics.ui.CredentialPatternView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/auth_credential_password_view.xml b/packages/SystemUI/res/layout/auth_credential_password_view.xml
index 774b335f..021ebe6 100644
--- a/packages/SystemUI/res/layout/auth_credential_password_view.xml
+++ b/packages/SystemUI/res/layout/auth_credential_password_view.xml
@@ -14,7 +14,7 @@
   ~ limitations under the License.
   -->
 
-<com.android.systemui.biometrics.AuthCredentialPasswordView
+<com.android.systemui.biometrics.ui.CredentialPasswordView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
@@ -83,4 +83,4 @@
 
     </LinearLayout>
 
-</com.android.systemui.biometrics.AuthCredentialPasswordView>
\ No newline at end of file
+</com.android.systemui.biometrics.ui.CredentialPasswordView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
index 4af9970..891c6af 100644
--- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
+++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
@@ -14,7 +14,7 @@
   ~ limitations under the License.
   -->
 
-<com.android.systemui.biometrics.AuthCredentialPatternView
+<com.android.systemui.biometrics.ui.CredentialPatternView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
@@ -78,4 +78,4 @@
             android:layout_gravity="center_horizontal|bottom"/>
     </FrameLayout>
 
-</com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file
+</com.android.systemui.biometrics.ui.CredentialPatternView>
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index 92ef3f8..159323a 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -42,12 +42,6 @@
         android:clipToPadding="false"
         android:clipChildren="false">
 
-        <ViewStub
-            android:id="@+id/qs_header_stub"
-            android:layout_height="wrap_content"
-            android:layout_width="match_parent"
-        />
-
         <include
             layout="@layout/keyguard_status_view"
             android:visibility="gone"/>
@@ -69,6 +63,15 @@
             systemui:layout_constraintBottom_toBottomOf="parent"
         />
 
+        <!-- This view should be after qs_frame so touches are dispatched first to it. That gives
+             it a chance to capture clicks before the NonInterceptingScrollView disallows all
+             intercepts -->
+        <ViewStub
+            android:id="@+id/qs_header_stub"
+            android:layout_height="wrap_content"
+            android:layout_width="match_parent"
+        />
+
         <androidx.constraintlayout.widget.Guideline
             android:id="@+id/qs_edge_guideline"
             android:layout_width="wrap_content"
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 7ca42f7..4fd25a9 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -177,6 +177,7 @@
     <item type="id" name="action_move_bottom_right"/>
     <item type="id" name="action_move_to_edge_and_hide"/>
     <item type="id" name="action_move_out_edge_and_show"/>
+    <item type="id" name="action_remove_menu"/>
 
     <!-- rounded corner view id -->
     <item type="id" name="rounded_corner_top_left"/>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index d5d99cc..b325c56 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2218,6 +2218,8 @@
     <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half">Move to edge and hide</string>
     <!-- Action in accessibility menu to move the accessibility floating button out the edge and show. [CHAR LIMIT=36]-->
     <string name="accessibility_floating_button_action_move_out_edge_and_show">Move out edge and show</string>
+    <!-- Action in accessibility menu to remove the accessibility floating menu view on the screen. [CHAR LIMIT=36]-->
+    <string name="accessibility_floating_button_action_remove_menu">Remove</string>
     <!-- Action in accessibility menu to toggle on/off the accessibility feature. [CHAR LIMIT=30]-->
     <string name="accessibility_floating_button_action_double_tap_to_toggle">toggle</string>
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java
index 7e42e1b..8ac1de8 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java
@@ -85,13 +85,12 @@
         mTmpSourceRectF.set(sourceBounds);
         mTmpDestinationRect.set(sourceBounds);
         mTmpDestinationRect.inset(insets);
-        // Scale by the shortest edge and offset such that the top/left of the scaled inset
-        // source rect aligns with the top/left of the destination bounds
+        // Scale to the bounds no smaller than the destination and offset such that the top/left
+        // of the scaled inset source rect aligns with the top/left of the destination bounds
         final float scale;
         if (sourceRectHint.isEmpty() || sourceRectHint.width() == sourceBounds.width()) {
-            scale = sourceBounds.width() <= sourceBounds.height()
-                    ? (float) destinationBounds.width() / sourceBounds.width()
-                    : (float) destinationBounds.height() / sourceBounds.height();
+            scale = Math.max((float) destinationBounds.width() / sourceBounds.width(),
+                    (float) destinationBounds.height() / sourceBounds.height());
         } else {
             // scale by sourceRectHint if it's not edge-to-edge
             final float endScale = sourceRectHint.width() <= sourceRectHint.height()
diff --git a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt
index bb3df8f..7b216017 100644
--- a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt
+++ b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt
@@ -18,17 +18,15 @@
 
 import android.content.Context
 import android.os.Handler
-import com.android.internal.statusbar.IStatusBarService
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.flags.FeatureFlagsDebug.ALL_FLAGS
 import com.android.systemui.util.settings.SettingsUtilModule
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
-import javax.inject.Named
 
 @Module(includes = [
     FeatureFlagsDebugStartableModule::class,
+    FlagsCommonModule::class,
     ServerFlagReaderModule::class,
     SettingsUtilModule::class,
 ])
@@ -43,20 +41,5 @@
         fun provideFlagManager(context: Context, @Main handler: Handler): FlagManager {
             return FlagManager(context, handler)
         }
-
-        @JvmStatic
-        @Provides
-        @Named(ALL_FLAGS)
-        fun providesAllFlags(): Map<Int, Flag<*>> = Flags.collectFlags()
-
-        @JvmStatic
-        @Provides
-        fun providesRestarter(barService: IStatusBarService): Restarter {
-            return object: Restarter {
-                override fun restart() {
-                    barService.restart()
-                }
-            }
-        }
     }
 }
diff --git a/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt
index 0f7e732..aef8876 100644
--- a/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt
+++ b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt
@@ -16,29 +16,15 @@
 
 package com.android.systemui.flags
 
-import com.android.internal.statusbar.IStatusBarService
 import dagger.Binds
 import dagger.Module
-import dagger.Provides
 
 @Module(includes = [
     FeatureFlagsReleaseStartableModule::class,
+    FlagsCommonModule::class,
     ServerFlagReaderModule::class
 ])
 abstract class FlagsModule {
     @Binds
     abstract fun bindsFeatureFlagRelease(impl: FeatureFlagsRelease): FeatureFlags
-
-    @Module
-    companion object {
-        @JvmStatic
-        @Provides
-        fun providesRestarter(barService: IStatusBarService): Restarter {
-            return object: Restarter {
-                override fun restart() {
-                    barService.restart()
-                }
-            }
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 81305f9..0b395a8 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -223,7 +223,7 @@
         @Override
         public void onSwipeUp() {
             if (!mUpdateMonitor.isFaceDetectionRunning()) {
-                boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(true,
+                boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(
                         FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER);
                 mKeyguardSecurityCallback.userActivity();
                 if (didFaceAuthRun) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index cc1f2fe..39dc609 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -1615,7 +1615,7 @@
                 @Override
                 public void onUdfpsPointerDown(int sensorId) {
                     mLogger.logUdfpsPointerDown(sensorId);
-                    requestFaceAuth(true, FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
+                    requestFaceAuth(FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
                 }
 
                 /**
@@ -2357,14 +2357,12 @@
     /**
      * Requests face authentication if we're on a state where it's allowed.
      * This will re-trigger auth in case it fails.
-     * @param userInitiatedRequest true if the user explicitly requested face auth
      * @param reason One of the reasons {@link FaceAuthApiRequestReason} on why this API is being
      * invoked.
      * @return current face auth detection state, true if it is running.
      */
-    public boolean requestFaceAuth(boolean userInitiatedRequest,
-            @FaceAuthApiRequestReason String reason) {
-        mLogger.logFaceAuthRequested(userInitiatedRequest, reason);
+    public boolean requestFaceAuth(@FaceAuthApiRequestReason String reason) {
+        mLogger.logFaceAuthRequested(reason);
         updateFaceListeningState(BIOMETRIC_ACTION_START, apiRequestReasonToUiEvent(reason));
         return isFaceDetectionRunning();
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 31fc320..3308f55 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -111,11 +111,10 @@
                 }, { "Face help received, msgId: $int1 msg: $str1" })
     }
 
-    fun logFaceAuthRequested(userInitiatedRequest: Boolean, reason: String?) {
+    fun logFaceAuthRequested(reason: String?) {
         logBuffer.log(TAG, DEBUG, {
-            bool1 = userInitiatedRequest
             str1 = reason
-        }, { "requestFaceAuth() userInitiated=$bool1 reason=$str1" })
+        }, { "requestFaceAuth() reason=$str1" })
     }
 
     fun logFaceAuthSuccess(userId: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java
index f56a154..ee048e1 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java
@@ -172,4 +172,8 @@
                 R.dimen.dismiss_circle_small);
         mSizePercent = mMinDismissSize / maxDismissSize;
     }
+
+    interface DismissCallback {
+        void onDismiss();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
index eb25232..396f584 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
@@ -35,6 +35,8 @@
 import androidx.dynamicanimation.animation.SpringForce;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.android.internal.util.Preconditions;
+
 import java.util.HashMap;
 
 /**
@@ -64,6 +66,7 @@
     private final Handler mHandler;
     private boolean mIsMovedToEdge;
     private boolean mIsFadeEffectEnabled;
+    private DismissAnimationController.DismissCallback mDismissCallback;
 
     // Cache the animations state of {@link DynamicAnimation.TRANSLATION_X} and {@link
     // DynamicAnimation.TRANSLATION_Y} to be well controlled by the touch handler
@@ -102,6 +105,11 @@
         }
     }
 
+    void setDismissCallback(
+            DismissAnimationController.DismissCallback dismissCallback) {
+        mDismissCallback = dismissCallback;
+    }
+
     void moveToTopLeftPosition() {
         mIsMovedToEdge = false;
         final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
@@ -132,6 +140,13 @@
         constrainPositionAndUpdate(position);
     }
 
+    void removeMenu() {
+        Preconditions.checkArgument(mDismissCallback != null,
+                "The dismiss callback should be initialized first.");
+
+        mDismissCallback.onDismiss();
+    }
+
     void flingMenuThenSpringToEdge(float x, float velocityX, float velocityY) {
         final boolean shouldMenuFlingLeft = isOnLeftSide()
                 ? velocityX < ESCAPE_VELOCITY
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java
index e69a248..ac5736b 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java
@@ -84,6 +84,12 @@
                 new AccessibilityNodeInfoCompat.AccessibilityActionCompat(moveEdgeId,
                         res.getString(moveEdgeTextResId));
         info.addAction(moveToOrOutEdge);
+
+        final AccessibilityNodeInfoCompat.AccessibilityActionCompat removeMenu =
+                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+                        R.id.action_remove_menu,
+                        res.getString(R.string.accessibility_floating_button_action_remove_menu));
+        info.addAction(removeMenu);
     }
 
     @Override
@@ -126,6 +132,11 @@
             return true;
         }
 
+        if (action == R.id.action_remove_menu) {
+            mAnimationController.removeMenu();
+            return true;
+        }
+
         return super.performAccessibilityAction(host, action, args);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index 3e620a2..33e155d 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -100,6 +100,7 @@
                 windowManager);
         mMenuView = new MenuView(context, menuViewModel, menuViewAppearance);
         mMenuAnimationController = mMenuView.getMenuAnimationController();
+        mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage);
 
         mDismissView = new DismissView(context);
         mDismissAnimationController = new DismissAnimationController(mDismissView, mMenuView);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index b50bfd7..f74c721 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -26,6 +26,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.AlertDialog;
 import android.content.Context;
 import android.graphics.PixelFormat;
 import android.hardware.biometrics.BiometricAuthenticator.Modality;
@@ -63,6 +64,9 @@
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.biometrics.AuthController.ScaleFactorProvider;
+import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
+import com.android.systemui.biometrics.ui.CredentialView;
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -74,11 +78,13 @@
 import java.util.List;
 import java.util.Set;
 
+import javax.inject.Provider;
+
 /**
  * Top level container/controller for the BiometricPrompt UI.
  */
 public class AuthContainerView extends LinearLayout
-        implements AuthDialog, WakefulnessLifecycle.Observer {
+        implements AuthDialog, WakefulnessLifecycle.Observer, CredentialView.Host {
 
     private static final String TAG = "AuthContainerView";
 
@@ -112,15 +118,18 @@
     private final IBinder mWindowToken = new Binder();
     private final WindowManager mWindowManager;
     private final Interpolator mLinearOutSlowIn;
-    private final CredentialCallback mCredentialCallback;
     private final LockPatternUtils mLockPatternUtils;
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     private final InteractionJankMonitor mInteractionJankMonitor;
 
+    // TODO: these should be migrated out once ready
+    private final Provider<BiometricPromptCredentialInteractor> mBiometricPromptInteractor;
+    private final Provider<CredentialViewModel> mCredentialViewModelProvider;
+
     @VisibleForTesting final BiometricCallback mBiometricCallback;
 
     @Nullable private AuthBiometricView mBiometricView;
-    @Nullable private AuthCredentialView mCredentialView;
+    @Nullable private View mCredentialView;
     private final AuthPanelController mPanelController;
     private final FrameLayout mFrameLayout;
     private final ImageView mBackgroundView;
@@ -229,11 +238,13 @@
                 @NonNull WakefulnessLifecycle wakefulnessLifecycle,
                 @NonNull UserManager userManager,
                 @NonNull LockPatternUtils lockPatternUtils,
-                @NonNull InteractionJankMonitor jankMonitor) {
+                @NonNull InteractionJankMonitor jankMonitor,
+                @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor,
+                @NonNull Provider<CredentialViewModel> credentialViewModelProvider) {
             mConfig.mSensorIds = sensorIds;
             return new AuthContainerView(mConfig, fpProps, faceProps, wakefulnessLifecycle,
-                    userManager, lockPatternUtils, jankMonitor, new Handler(Looper.getMainLooper()),
-                    bgExecutor);
+                    userManager, lockPatternUtils, jankMonitor, biometricPromptInteractor,
+                    credentialViewModelProvider, new Handler(Looper.getMainLooper()), bgExecutor);
         }
     }
 
@@ -271,14 +282,51 @@
         }
     }
 
-    final class CredentialCallback implements AuthCredentialView.Callback {
-        @Override
-        public void onCredentialMatched(byte[] attestation) {
-            mCredentialAttestation = attestation;
-            animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED);
+    @Override
+    public void onCredentialMatched(@NonNull byte[] attestation) {
+        mCredentialAttestation = attestation;
+        animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED);
+    }
+
+    @Override
+    public void onCredentialAborted() {
+        sendEarlyUserCanceled();
+        animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
+    }
+
+    @Override
+    public void onCredentialAttemptsRemaining(int remaining, @NonNull String messageBody) {
+        // Only show dialog if <=1 attempts are left before wiping.
+        if (remaining == 1) {
+            showLastAttemptBeforeWipeDialog(messageBody);
+        } else if (remaining <= 0) {
+            showNowWipingDialog(messageBody);
         }
     }
 
+    private void showLastAttemptBeforeWipeDialog(@NonNull String messageBody) {
+        final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
+                .setTitle(R.string.biometric_dialog_last_attempt_before_wipe_dialog_title)
+                .setMessage(messageBody)
+                .setPositiveButton(android.R.string.ok, null)
+                .create();
+        alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
+        alertDialog.show();
+    }
+
+    private void showNowWipingDialog(@NonNull String messageBody) {
+        final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
+                .setMessage(messageBody)
+                .setPositiveButton(
+                        com.android.settingslib.R.string.failed_attempts_now_wiping_dialog_dismiss,
+                        null /* OnClickListener */)
+                .setOnDismissListener(
+                        dialog -> animateAway(AuthDialogCallback.DISMISSED_ERROR))
+                .create();
+        alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
+        alertDialog.show();
+    }
+
     @VisibleForTesting
     AuthContainerView(Config config,
             @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
@@ -287,6 +335,8 @@
             @NonNull UserManager userManager,
             @NonNull LockPatternUtils lockPatternUtils,
             @NonNull InteractionJankMonitor jankMonitor,
+            @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor,
+            @NonNull Provider<CredentialViewModel> credentialViewModelProvider,
             @NonNull Handler mainHandler,
             @NonNull @Background DelayableExecutor bgExecutor) {
         super(config.mContext);
@@ -302,7 +352,6 @@
                 .getDimension(R.dimen.biometric_dialog_animation_translation_offset);
         mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
         mBiometricCallback = new BiometricCallback();
-        mCredentialCallback = new CredentialCallback();
 
         final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
         mFrameLayout = (FrameLayout) layoutInflater.inflate(
@@ -314,6 +363,8 @@
         mPanelController = new AuthPanelController(mContext, mPanelView);
         mBackgroundExecutor = bgExecutor;
         mInteractionJankMonitor = jankMonitor;
+        mBiometricPromptInteractor = biometricPromptInteractor;
+        mCredentialViewModelProvider = credentialViewModelProvider;
 
         // Inflate biometric view only if necessary.
         if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
@@ -404,12 +455,12 @@
 
         switch (credentialType) {
             case Utils.CREDENTIAL_PATTERN:
-                mCredentialView = (AuthCredentialView) factory.inflate(
+                mCredentialView = factory.inflate(
                         R.layout.auth_credential_pattern_view, null, false);
                 break;
             case Utils.CREDENTIAL_PIN:
             case Utils.CREDENTIAL_PASSWORD:
-                mCredentialView = (AuthCredentialView) factory.inflate(
+                mCredentialView = factory.inflate(
                         R.layout.auth_credential_password_view, null, false);
                 break;
             default:
@@ -422,16 +473,12 @@
         mBackgroundView.setOnClickListener(null);
         mBackgroundView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
 
-        mCredentialView.setContainerView(this);
-        mCredentialView.setUserId(mConfig.mUserId);
-        mCredentialView.setOperationId(mConfig.mOperationId);
-        mCredentialView.setEffectiveUserId(mEffectiveUserId);
-        mCredentialView.setCredentialType(credentialType);
-        mCredentialView.setCallback(mCredentialCallback);
-        mCredentialView.setPromptInfo(mConfig.mPromptInfo);
-        mCredentialView.setPanelController(mPanelController, animatePanel);
-        mCredentialView.setShouldAnimateContents(animateContents);
-        mCredentialView.setBackgroundExecutor(mBackgroundExecutor);
+        mBiometricPromptInteractor.get().useCredentialsForAuthentication(
+                mConfig.mPromptInfo, credentialType, mConfig.mUserId, mConfig.mOperationId);
+        final CredentialViewModel vm = mCredentialViewModelProvider.get();
+        vm.setAnimateContents(animateContents);
+        ((CredentialView) mCredentialView).init(vm, this, mPanelController, animatePanel);
+
         mFrameLayout.addView(mCredentialView);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 9c78df5..313ff4157 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -72,6 +72,8 @@
 import com.android.internal.os.SomeArgs;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.systemui.CoreStartable;
+import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -122,6 +124,10 @@
     private final Provider<UdfpsController> mUdfpsControllerFactory;
     private final Provider<SidefpsController> mSidefpsControllerFactory;
 
+    // TODO: these should be migrated out once ready
+    @NonNull private final Provider<BiometricPromptCredentialInteractor> mBiometricPromptInteractor;
+    @NonNull private final Provider<CredentialViewModel> mCredentialViewModelProvider;
+
     private final Display mDisplay;
     private float mScaleFactor = 1f;
     // sensor locations without any resolution scaling nor rotation adjustments:
@@ -693,6 +699,8 @@
             @NonNull LockPatternUtils lockPatternUtils,
             @NonNull UdfpsLogger udfpsLogger,
             @NonNull StatusBarStateController statusBarStateController,
+            @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor,
+            @NonNull Provider<CredentialViewModel> credentialViewModelProvider,
             @NonNull InteractionJankMonitor jankMonitor,
             @Main Handler handler,
             @Background DelayableExecutor bgExecutor,
@@ -717,6 +725,9 @@
         mFaceEnrolledForUser = new SparseBooleanArray();
         mVibratorHelper = vibrator;
 
+        mBiometricPromptInteractor = biometricPromptInteractor;
+        mCredentialViewModelProvider = credentialViewModelProvider;
+
         mOrientationListener = new BiometricDisplayListener(
                 context,
                 mDisplayManager,
@@ -1079,6 +1090,11 @@
         return mUdfpsEnrolledForUser.get(userId);
     }
 
+    /** If BiometricPrompt is currently being shown to the user. */
+    public boolean isShowing() {
+        return mCurrentDialog != null;
+    }
+
     private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
         mCurrentDialogArgs = args;
 
@@ -1210,7 +1226,8 @@
                 .setMultiSensorConfig(multiSensorConfig)
                 .setScaleFactorProvider(() -> getScaleFactor())
                 .build(bgExecutor, sensorIds, mFpProps, mFaceProps, wakefulnessLifecycle,
-                        userManager, lockPatternUtils, mInteractionJankMonitor);
+                        userManager, lockPatternUtils, mInteractionJankMonitor,
+                        mBiometricPromptInteractor, mCredentialViewModelProvider);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java
deleted file mode 100644
index 76cd3f4..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * Copyright (C) 2019 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 static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
-import static android.view.WindowInsets.Type.ime;
-
-import android.annotation.NonNull;
-import android.content.Context;
-import android.graphics.Insets;
-import android.os.UserHandle;
-import android.text.InputType;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.View.OnApplyWindowInsetsListener;
-import android.view.ViewGroup;
-import android.view.WindowInsets;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.ImeAwareEditText;
-import android.widget.TextView;
-
-import com.android.internal.widget.LockPatternChecker;
-import com.android.internal.widget.LockPatternUtils;
-import com.android.internal.widget.LockscreenCredential;
-import com.android.internal.widget.VerifyCredentialResponse;
-import com.android.systemui.Dumpable;
-import com.android.systemui.R;
-
-import java.io.PrintWriter;
-
-/**
- * Pin and Password UI
- */
-public class AuthCredentialPasswordView extends AuthCredentialView
-        implements TextView.OnEditorActionListener, OnApplyWindowInsetsListener, Dumpable {
-
-    private static final String TAG = "BiometricPrompt/AuthCredentialPasswordView";
-
-    private final InputMethodManager mImm;
-    private ImeAwareEditText mPasswordField;
-    private ViewGroup mAuthCredentialHeader;
-    private ViewGroup mAuthCredentialInput;
-    private int mBottomInset = 0;
-
-    public AuthCredentialPasswordView(Context context,
-            AttributeSet attrs) {
-        super(context, attrs);
-        mImm = mContext.getSystemService(InputMethodManager.class);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-
-        mAuthCredentialHeader = findViewById(R.id.auth_credential_header);
-        mAuthCredentialInput = findViewById(R.id.auth_credential_input);
-        mPasswordField = findViewById(R.id.lockPassword);
-        mPasswordField.setOnEditorActionListener(this);
-        // TODO: De-dupe the logic with AuthContainerView
-        mPasswordField.setOnKeyListener((v, keyCode, event) -> {
-            if (keyCode != KeyEvent.KEYCODE_BACK) {
-                return false;
-            }
-            if (event.getAction() == KeyEvent.ACTION_UP) {
-                mContainerView.sendEarlyUserCanceled();
-                mContainerView.animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
-            }
-            return true;
-        });
-
-        setOnApplyWindowInsetsListener(this);
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-
-        mPasswordField.setTextOperationUser(UserHandle.of(mUserId));
-        if (mCredentialType == Utils.CREDENTIAL_PIN) {
-            mPasswordField.setInputType(
-                    InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
-        }
-
-        mPasswordField.requestFocus();
-        mPasswordField.scheduleShowSoftInput();
-    }
-
-    @Override
-    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
-        // Check if this was the result of hitting the enter key
-        final boolean isSoftImeEvent = event == null
-                && (actionId == EditorInfo.IME_NULL
-                || actionId == EditorInfo.IME_ACTION_DONE
-                || actionId == EditorInfo.IME_ACTION_NEXT);
-        final boolean isKeyboardEnterKey = event != null
-                && KeyEvent.isConfirmKey(event.getKeyCode())
-                && event.getAction() == KeyEvent.ACTION_DOWN;
-        if (isSoftImeEvent || isKeyboardEnterKey) {
-            checkPasswordAndUnlock();
-            return true;
-        }
-        return false;
-    }
-
-    private void checkPasswordAndUnlock() {
-        try (LockscreenCredential password = mCredentialType == Utils.CREDENTIAL_PIN
-                ? LockscreenCredential.createPinOrNone(mPasswordField.getText())
-                : LockscreenCredential.createPasswordOrNone(mPasswordField.getText())) {
-            if (password.isNone()) {
-                return;
-            }
-
-            // Request LockSettingsService to return the Gatekeeper Password in the
-            // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the
-            // Gatekeeper Password and operationId.
-            mPendingLockCheck = LockPatternChecker.verifyCredential(mLockPatternUtils,
-                    password, mEffectiveUserId, LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE,
-                    this::onCredentialVerified);
-        }
-    }
-
-    @Override
-    protected void onCredentialVerified(@NonNull VerifyCredentialResponse response,
-            int timeoutMs) {
-        super.onCredentialVerified(response, timeoutMs);
-
-        if (response.isMatched()) {
-            mImm.hideSoftInputFromWindow(getWindowToken(), 0 /* flags */);
-        } else {
-            mPasswordField.setText("");
-        }
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        super.onLayout(changed, left, top, right, bottom);
-
-        if (mAuthCredentialInput == null || mAuthCredentialHeader == null || mSubtitleView == null
-                || mDescriptionView == null || mPasswordField == null || mErrorView == null) {
-            return;
-        }
-
-        int inputLeftBound;
-        int inputTopBound;
-        int headerRightBound = right;
-        int headerTopBounds = top;
-        final int subTitleBottom = (mSubtitleView.getVisibility() == GONE) ? mTitleView.getBottom()
-                : mSubtitleView.getBottom();
-        final int descBottom = (mDescriptionView.getVisibility() == GONE) ? subTitleBottom
-                : mDescriptionView.getBottom();
-        if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
-            inputTopBound = (bottom - mAuthCredentialInput.getHeight()) / 2;
-            inputLeftBound = (right - left) / 2;
-            headerRightBound = inputLeftBound;
-            headerTopBounds -= Math.min(mIconView.getBottom(), mBottomInset);
-        } else {
-            inputTopBound =
-                    descBottom + (bottom - descBottom - mAuthCredentialInput.getHeight()) / 2;
-            inputLeftBound = (right - left - mAuthCredentialInput.getWidth()) / 2;
-        }
-
-        if (mDescriptionView.getBottom() > mBottomInset) {
-            mAuthCredentialHeader.layout(left, headerTopBounds, headerRightBound, bottom);
-        }
-        mAuthCredentialInput.layout(inputLeftBound, inputTopBound, right, bottom);
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-        final int newWidth = MeasureSpec.getSize(widthMeasureSpec);
-        final int newHeight = MeasureSpec.getSize(heightMeasureSpec) - mBottomInset;
-
-        setMeasuredDimension(newWidth, newHeight);
-
-        final int halfWidthSpec = MeasureSpec.makeMeasureSpec(getWidth() / 2,
-                MeasureSpec.AT_MOST);
-        final int fullHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.UNSPECIFIED);
-        if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
-            measureChildren(halfWidthSpec, fullHeightSpec);
-        } else {
-            measureChildren(widthMeasureSpec, fullHeightSpec);
-        }
-    }
-
-    @NonNull
-    @Override
-    public WindowInsets onApplyWindowInsets(@NonNull View v, WindowInsets insets) {
-
-        final Insets bottomInset = insets.getInsets(ime());
-        if (v instanceof AuthCredentialPasswordView && mBottomInset != bottomInset.bottom) {
-            mBottomInset = bottomInset.bottom;
-            if (mBottomInset > 0
-                    && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
-                mTitleView.setSingleLine(true);
-                mTitleView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
-                mTitleView.setMarqueeRepeatLimit(-1);
-                // select to enable marquee unless a screen reader is enabled
-                mTitleView.setSelected(!mAccessibilityManager.isEnabled()
-                        || !mAccessibilityManager.isTouchExplorationEnabled());
-            } else {
-                mTitleView.setSingleLine(false);
-                mTitleView.setEllipsize(null);
-                // select to enable marquee unless a screen reader is enabled
-                mTitleView.setSelected(false);
-            }
-            requestLayout();
-        }
-        return insets;
-    }
-
-    @Override
-    public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
-        pw.println(TAG + "State:");
-        pw.println("  mBottomInset=" + mBottomInset);
-        pw.println("  mAuthCredentialHeader size=(" + mAuthCredentialHeader.getWidth() + ","
-                + mAuthCredentialHeader.getHeight());
-        pw.println("  mAuthCredentialInput size=(" + mAuthCredentialInput.getWidth() + ","
-                + mAuthCredentialInput.getHeight());
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java
deleted file mode 100644
index f9e44a0..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright (C) 2019 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.NonNull;
-import android.content.Context;
-import android.util.AttributeSet;
-
-import com.android.internal.widget.LockPatternChecker;
-import com.android.internal.widget.LockPatternUtils;
-import com.android.internal.widget.LockPatternView;
-import com.android.internal.widget.LockscreenCredential;
-import com.android.internal.widget.VerifyCredentialResponse;
-import com.android.systemui.R;
-
-import java.util.List;
-
-/**
- * Pattern UI
- */
-public class AuthCredentialPatternView extends AuthCredentialView {
-
-    private LockPatternView mLockPatternView;
-
-    private class UnlockPatternListener implements LockPatternView.OnPatternListener {
-
-        @Override
-        public void onPatternStart() {
-
-        }
-
-        @Override
-        public void onPatternCleared() {
-
-        }
-
-        @Override
-        public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
-
-        }
-
-        @Override
-        public void onPatternDetected(List<LockPatternView.Cell> pattern) {
-            if (mPendingLockCheck != null) {
-                mPendingLockCheck.cancel(false);
-            }
-
-            mLockPatternView.setEnabled(false);
-
-            if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
-                // Pattern size is less than the minimum, do not count it as a failed attempt.
-                onPatternVerified(VerifyCredentialResponse.ERROR, 0 /* timeoutMs */);
-                return;
-            }
-
-            try (LockscreenCredential credential = LockscreenCredential.createPattern(pattern)) {
-                // Request LockSettingsService to return the Gatekeeper Password in the
-                // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the
-                // Gatekeeper Password and operationId.
-                mPendingLockCheck = LockPatternChecker.verifyCredential(
-                        mLockPatternUtils,
-                        credential,
-                        mEffectiveUserId,
-                        LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE,
-                        this::onPatternVerified);
-            }
-        }
-
-        private void onPatternVerified(@NonNull VerifyCredentialResponse response, int timeoutMs) {
-            AuthCredentialPatternView.this.onCredentialVerified(response, timeoutMs);
-            if (timeoutMs > 0) {
-                mLockPatternView.setEnabled(false);
-            } else {
-                mLockPatternView.setEnabled(true);
-            }
-        }
-    }
-
-    @Override
-    protected void onErrorTimeoutFinish() {
-        super.onErrorTimeoutFinish();
-        // select to enable marquee unless a screen reader is enabled
-        mLockPatternView.setEnabled(!mAccessibilityManager.isEnabled()
-                || !mAccessibilityManager.isTouchExplorationEnabled());
-    }
-
-    public AuthCredentialPatternView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        mLockPatternView = findViewById(R.id.lockPattern);
-        mLockPatternView.setOnPatternListener(new UnlockPatternListener());
-        mLockPatternView.setInStealthMode(
-                !mLockPatternUtils.isVisiblePatternEnabled(mUserId));
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
deleted file mode 100644
index fa623d1..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
+++ /dev/null
@@ -1,565 +0,0 @@
-/*
- * Copyright (C) 2019 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 static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS;
-import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT;
-import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PATTERN_LAST_ATTEMPT;
-import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PIN_LAST_ATTEMPT;
-import static android.app.admin.DevicePolicyResources.UNDEFINED;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.AlertDialog;
-import android.app.admin.DevicePolicyManager;
-import android.content.Context;
-import android.content.pm.UserInfo;
-import android.graphics.drawable.Drawable;
-import android.hardware.biometrics.BiometricPrompt;
-import android.hardware.biometrics.PromptInfo;
-import android.os.AsyncTask;
-import android.os.CountDownTimer;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.SystemClock;
-import android.os.UserManager;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.StringRes;
-
-import com.android.internal.widget.LockPatternUtils;
-import com.android.internal.widget.VerifyCredentialResponse;
-import com.android.systemui.R;
-import com.android.systemui.animation.Interpolators;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.util.concurrency.DelayableExecutor;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Abstract base class for Pin, Pattern, or Password authentication, for
- * {@link BiometricPrompt.Builder#setAllowedAuthenticators(int)}}
- */
-public abstract class AuthCredentialView extends LinearLayout {
-    private static final String TAG = "BiometricPrompt/AuthCredentialView";
-    private static final int ERROR_DURATION_MS = 3000;
-
-    static final int USER_TYPE_PRIMARY = 1;
-    static final int USER_TYPE_MANAGED_PROFILE = 2;
-    static final int USER_TYPE_SECONDARY = 3;
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef({USER_TYPE_PRIMARY, USER_TYPE_MANAGED_PROFILE, USER_TYPE_SECONDARY})
-    private @interface UserType {}
-
-    protected final Handler mHandler;
-    protected final LockPatternUtils mLockPatternUtils;
-
-    protected final AccessibilityManager mAccessibilityManager;
-    private final UserManager mUserManager;
-    private final DevicePolicyManager mDevicePolicyManager;
-
-    private PromptInfo mPromptInfo;
-    private AuthPanelController mPanelController;
-    private boolean mShouldAnimatePanel;
-    private boolean mShouldAnimateContents;
-
-    protected TextView mTitleView;
-    protected TextView mSubtitleView;
-    protected TextView mDescriptionView;
-    protected ImageView mIconView;
-    protected TextView mErrorView;
-
-    protected @Utils.CredentialType int mCredentialType;
-    protected AuthContainerView mContainerView;
-    protected Callback mCallback;
-    protected AsyncTask<?, ?, ?> mPendingLockCheck;
-    protected int mUserId;
-    protected long mOperationId;
-    protected int mEffectiveUserId;
-    protected ErrorTimer mErrorTimer;
-
-    protected @Background DelayableExecutor mBackgroundExecutor;
-
-    interface Callback {
-        void onCredentialMatched(byte[] attestation);
-    }
-
-    protected static class ErrorTimer extends CountDownTimer {
-        private final TextView mErrorView;
-        private final Context mContext;
-
-        /**
-         * @param millisInFuture    The number of millis in the future from the call
-         *                          to {@link #start()} until the countdown is done and {@link
-         *                          #onFinish()}
-         *                          is called.
-         * @param countDownInterval The interval along the way to receive
-         *                          {@link #onTick(long)} callbacks.
-         */
-        public ErrorTimer(Context context, long millisInFuture, long countDownInterval,
-                TextView errorView) {
-            super(millisInFuture, countDownInterval);
-            mErrorView = errorView;
-            mContext = context;
-        }
-
-        @Override
-        public void onTick(long millisUntilFinished) {
-            final int secondsCountdown = (int) (millisUntilFinished / 1000);
-            mErrorView.setText(mContext.getString(
-                    R.string.biometric_dialog_credential_too_many_attempts, secondsCountdown));
-        }
-
-        @Override
-        public void onFinish() {
-            if (mErrorView != null) {
-                mErrorView.setText("");
-            }
-        }
-    }
-
-    protected final Runnable mClearErrorRunnable = new Runnable() {
-        @Override
-        public void run() {
-            if (mErrorView != null) {
-                mErrorView.setText("");
-            }
-        }
-    };
-
-    public AuthCredentialView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-
-        mLockPatternUtils = new LockPatternUtils(mContext);
-        mHandler = new Handler(Looper.getMainLooper());
-        mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
-        mUserManager = mContext.getSystemService(UserManager.class);
-        mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class);
-    }
-
-    protected void showError(String error) {
-        if (mHandler != null) {
-            mHandler.removeCallbacks(mClearErrorRunnable);
-            mHandler.postDelayed(mClearErrorRunnable, ERROR_DURATION_MS);
-        }
-        if (mErrorView != null) {
-            mErrorView.setText(error);
-        }
-    }
-
-    private void setTextOrHide(TextView view, CharSequence text) {
-        if (TextUtils.isEmpty(text)) {
-            view.setVisibility(View.GONE);
-        } else {
-            view.setText(text);
-        }
-
-        Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this);
-    }
-
-    private void setText(TextView view, CharSequence text) {
-        view.setText(text);
-    }
-
-    void setUserId(int userId) {
-        mUserId = userId;
-    }
-
-    void setOperationId(long operationId) {
-        mOperationId = operationId;
-    }
-
-    void setEffectiveUserId(int effectiveUserId) {
-        mEffectiveUserId = effectiveUserId;
-    }
-
-    void setCredentialType(@Utils.CredentialType int credentialType) {
-        mCredentialType = credentialType;
-    }
-
-    void setCallback(Callback callback) {
-        mCallback = callback;
-    }
-
-    void setPromptInfo(PromptInfo promptInfo) {
-        mPromptInfo = promptInfo;
-    }
-
-    void setPanelController(AuthPanelController panelController, boolean animatePanel) {
-        mPanelController = panelController;
-        mShouldAnimatePanel = animatePanel;
-    }
-
-    void setShouldAnimateContents(boolean animateContents) {
-        mShouldAnimateContents = animateContents;
-    }
-
-    void setContainerView(AuthContainerView containerView) {
-        mContainerView = containerView;
-    }
-
-    void setBackgroundExecutor(@Background DelayableExecutor bgExecutor) {
-        mBackgroundExecutor = bgExecutor;
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-
-        final CharSequence title = getTitle(mPromptInfo);
-        setText(mTitleView, title);
-        setTextOrHide(mSubtitleView, getSubtitle(mPromptInfo));
-        setTextOrHide(mDescriptionView, getDescription(mPromptInfo));
-        announceForAccessibility(title);
-
-        if (mIconView != null) {
-            final boolean isManagedProfile = Utils.isManagedProfile(mContext, mEffectiveUserId);
-            final Drawable image;
-            if (isManagedProfile) {
-                image = getResources().getDrawable(R.drawable.auth_dialog_enterprise,
-                        mContext.getTheme());
-            } else {
-                image = getResources().getDrawable(R.drawable.auth_dialog_lock,
-                        mContext.getTheme());
-            }
-            mIconView.setImageDrawable(image);
-        }
-
-        // Only animate this if we're transitioning from a biometric view.
-        if (mShouldAnimateContents) {
-            setTranslationY(getResources()
-                    .getDimension(R.dimen.biometric_dialog_credential_translation_offset));
-            setAlpha(0);
-
-            postOnAnimation(() -> {
-                animate().translationY(0)
-                        .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS)
-                        .alpha(1.f)
-                        .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
-                        .withLayer()
-                        .start();
-            });
-        }
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        if (mErrorTimer != null) {
-            mErrorTimer.cancel();
-        }
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mTitleView = findViewById(R.id.title);
-        mSubtitleView = findViewById(R.id.subtitle);
-        mDescriptionView = findViewById(R.id.description);
-        mIconView = findViewById(R.id.icon);
-        mErrorView = findViewById(R.id.error);
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        super.onLayout(changed, left, top, right, bottom);
-
-        if (mShouldAnimatePanel) {
-            // Credential view is always full screen.
-            mPanelController.setUseFullScreen(true);
-            mPanelController.updateForContentDimensions(mPanelController.getContainerWidth(),
-                    mPanelController.getContainerHeight(), 0 /* animateDurationMs */);
-            mShouldAnimatePanel = false;
-        }
-    }
-
-    protected void onErrorTimeoutFinish() {}
-
-    protected void onCredentialVerified(@NonNull VerifyCredentialResponse response, int timeoutMs) {
-        if (response.isMatched()) {
-            mClearErrorRunnable.run();
-            mLockPatternUtils.userPresent(mEffectiveUserId);
-
-            // The response passed into this method contains the Gatekeeper Password. We still
-            // have to request Gatekeeper to create a Hardware Auth Token with the
-            // Gatekeeper Password and Challenge (keystore operationId in this case)
-            final long pwHandle = response.getGatekeeperPasswordHandle();
-            final VerifyCredentialResponse gkResponse = mLockPatternUtils
-                    .verifyGatekeeperPasswordHandle(pwHandle, mOperationId, mEffectiveUserId);
-
-            mCallback.onCredentialMatched(gkResponse.getGatekeeperHAT());
-            mLockPatternUtils.removeGatekeeperPasswordHandle(pwHandle);
-        } else {
-            if (timeoutMs > 0) {
-                mHandler.removeCallbacks(mClearErrorRunnable);
-                long deadline = mLockPatternUtils.setLockoutAttemptDeadline(
-                        mEffectiveUserId, timeoutMs);
-                mErrorTimer = new ErrorTimer(mContext,
-                        deadline - SystemClock.elapsedRealtime(),
-                        LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS,
-                        mErrorView) {
-                    @Override
-                    public void onFinish() {
-                        onErrorTimeoutFinish();
-                        mClearErrorRunnable.run();
-                    }
-                };
-                mErrorTimer.start();
-            } else {
-                final boolean didUpdateErrorText = reportFailedAttempt();
-                if (!didUpdateErrorText) {
-                    final @StringRes int errorRes;
-                    switch (mCredentialType) {
-                        case Utils.CREDENTIAL_PIN:
-                            errorRes = R.string.biometric_dialog_wrong_pin;
-                            break;
-                        case Utils.CREDENTIAL_PATTERN:
-                            errorRes = R.string.biometric_dialog_wrong_pattern;
-                            break;
-                        case Utils.CREDENTIAL_PASSWORD:
-                        default:
-                            errorRes = R.string.biometric_dialog_wrong_password;
-                            break;
-                    }
-                    showError(getResources().getString(errorRes));
-                }
-            }
-        }
-    }
-
-    private boolean reportFailedAttempt() {
-        boolean result = updateErrorMessage(
-                mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId) + 1);
-        mLockPatternUtils.reportFailedPasswordAttempt(mEffectiveUserId);
-        return result;
-    }
-
-    private boolean updateErrorMessage(int numAttempts) {
-        // Don't show any message if there's no maximum number of attempts.
-        final int maxAttempts = mLockPatternUtils.getMaximumFailedPasswordsForWipe(
-                mEffectiveUserId);
-        if (maxAttempts <= 0 || numAttempts <= 0) {
-            return false;
-        }
-
-        // Update the on-screen error string.
-        if (mErrorView != null) {
-            final String message = getResources().getString(
-                    R.string.biometric_dialog_credential_attempts_before_wipe,
-                    numAttempts,
-                    maxAttempts);
-            showError(message);
-        }
-
-        // Only show dialog if <=1 attempts are left before wiping.
-        final int remainingAttempts = maxAttempts - numAttempts;
-        if (remainingAttempts == 1) {
-            showLastAttemptBeforeWipeDialog();
-        } else if (remainingAttempts <= 0) {
-            showNowWipingDialog();
-        }
-        return true;
-    }
-
-    private void showLastAttemptBeforeWipeDialog() {
-        mBackgroundExecutor.execute(() -> {
-            final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
-                    .setTitle(R.string.biometric_dialog_last_attempt_before_wipe_dialog_title)
-                    .setMessage(
-                            getLastAttemptBeforeWipeMessage(getUserTypeForWipe(), mCredentialType))
-                    .setPositiveButton(android.R.string.ok, null)
-                    .create();
-            alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
-            mHandler.post(alertDialog::show);
-        });
-    }
-
-    private void showNowWipingDialog() {
-        mBackgroundExecutor.execute(() -> {
-            String nowWipingMessage = getNowWipingMessage(getUserTypeForWipe());
-            final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
-                    .setMessage(nowWipingMessage)
-                    .setPositiveButton(
-                            com.android.settingslib.R.string.failed_attempts_now_wiping_dialog_dismiss,
-                            null /* OnClickListener */)
-                    .setOnDismissListener(
-                            dialog -> mContainerView.animateAway(
-                                    AuthDialogCallback.DISMISSED_ERROR))
-                    .create();
-            alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
-            mHandler.post(alertDialog::show);
-        });
-    }
-
-    private @UserType int getUserTypeForWipe() {
-        final UserInfo userToBeWiped = mUserManager.getUserInfo(
-                mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(mEffectiveUserId));
-        if (userToBeWiped == null || userToBeWiped.isPrimary()) {
-            return USER_TYPE_PRIMARY;
-        } else if (userToBeWiped.isManagedProfile()) {
-            return USER_TYPE_MANAGED_PROFILE;
-        } else {
-            return USER_TYPE_SECONDARY;
-        }
-    }
-
-    // This should not be called on the main thread to avoid making an IPC.
-    private String getLastAttemptBeforeWipeMessage(
-            @UserType int userType, @Utils.CredentialType int credentialType) {
-        switch (userType) {
-            case USER_TYPE_PRIMARY:
-                return getLastAttemptBeforeWipeDeviceMessage(credentialType);
-            case USER_TYPE_MANAGED_PROFILE:
-                return getLastAttemptBeforeWipeProfileMessage(credentialType);
-            case USER_TYPE_SECONDARY:
-                return getLastAttemptBeforeWipeUserMessage(credentialType);
-            default:
-                throw new IllegalArgumentException("Unrecognized user type:" + userType);
-        }
-    }
-
-    private String getLastAttemptBeforeWipeDeviceMessage(
-            @Utils.CredentialType int credentialType) {
-        switch (credentialType) {
-            case Utils.CREDENTIAL_PIN:
-                return mContext.getString(
-                        R.string.biometric_dialog_last_pin_attempt_before_wipe_device);
-            case Utils.CREDENTIAL_PATTERN:
-                return mContext.getString(
-                        R.string.biometric_dialog_last_pattern_attempt_before_wipe_device);
-            case Utils.CREDENTIAL_PASSWORD:
-            default:
-                return mContext.getString(
-                        R.string.biometric_dialog_last_password_attempt_before_wipe_device);
-        }
-    }
-
-    // This should not be called on the main thread to avoid making an IPC.
-    private String getLastAttemptBeforeWipeProfileMessage(
-            @Utils.CredentialType int credentialType) {
-        return mDevicePolicyManager.getResources().getString(
-                getLastAttemptBeforeWipeProfileUpdatableStringId(credentialType),
-                () -> getLastAttemptBeforeWipeProfileDefaultMessage(credentialType));
-    }
-
-    private static String getLastAttemptBeforeWipeProfileUpdatableStringId(
-            @Utils.CredentialType int credentialType) {
-        switch (credentialType) {
-            case Utils.CREDENTIAL_PIN:
-                return BIOMETRIC_DIALOG_WORK_PIN_LAST_ATTEMPT;
-            case Utils.CREDENTIAL_PATTERN:
-                return BIOMETRIC_DIALOG_WORK_PATTERN_LAST_ATTEMPT;
-            case Utils.CREDENTIAL_PASSWORD:
-            default:
-                return BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT;
-        }
-    }
-
-    private String getLastAttemptBeforeWipeProfileDefaultMessage(
-            @Utils.CredentialType int credentialType) {
-        int resId;
-        switch (credentialType) {
-            case Utils.CREDENTIAL_PIN:
-                resId = R.string.biometric_dialog_last_pin_attempt_before_wipe_profile;
-                break;
-            case Utils.CREDENTIAL_PATTERN:
-                resId = R.string.biometric_dialog_last_pattern_attempt_before_wipe_profile;
-                break;
-            case Utils.CREDENTIAL_PASSWORD:
-            default:
-                resId = R.string.biometric_dialog_last_password_attempt_before_wipe_profile;
-        }
-        return mContext.getString(resId);
-    }
-
-    private String getLastAttemptBeforeWipeUserMessage(
-            @Utils.CredentialType int credentialType) {
-        int resId;
-        switch (credentialType) {
-            case Utils.CREDENTIAL_PIN:
-                resId = R.string.biometric_dialog_last_pin_attempt_before_wipe_user;
-                break;
-            case Utils.CREDENTIAL_PATTERN:
-                resId = R.string.biometric_dialog_last_pattern_attempt_before_wipe_user;
-                break;
-            case Utils.CREDENTIAL_PASSWORD:
-            default:
-                resId = R.string.biometric_dialog_last_password_attempt_before_wipe_user;
-        }
-        return mContext.getString(resId);
-    }
-
-    private String getNowWipingMessage(@UserType int userType) {
-        return mDevicePolicyManager.getResources().getString(
-                getNowWipingUpdatableStringId(userType),
-                () -> getNowWipingDefaultMessage(userType));
-    }
-
-    private String getNowWipingUpdatableStringId(@UserType int userType) {
-        switch (userType) {
-            case USER_TYPE_MANAGED_PROFILE:
-                return BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS;
-            default:
-                return UNDEFINED;
-        }
-    }
-
-    private String getNowWipingDefaultMessage(@UserType int userType) {
-        int resId;
-        switch (userType) {
-            case USER_TYPE_PRIMARY:
-                resId = com.android.settingslib.R.string.failed_attempts_now_wiping_device;
-                break;
-            case USER_TYPE_MANAGED_PROFILE:
-                resId = com.android.settingslib.R.string.failed_attempts_now_wiping_profile;
-                break;
-            case USER_TYPE_SECONDARY:
-                resId = com.android.settingslib.R.string.failed_attempts_now_wiping_user;
-                break;
-            default:
-                throw new IllegalArgumentException("Unrecognized user type:" + userType);
-        }
-        return mContext.getString(resId);
-    }
-
-    @Nullable
-    private static CharSequence getTitle(@NonNull PromptInfo promptInfo) {
-        final CharSequence credentialTitle = promptInfo.getDeviceCredentialTitle();
-        return credentialTitle != null ? credentialTitle : promptInfo.getTitle();
-    }
-
-    @Nullable
-    private static CharSequence getSubtitle(@NonNull PromptInfo promptInfo) {
-        final CharSequence credentialSubtitle = promptInfo.getDeviceCredentialSubtitle();
-        return credentialSubtitle != null ? credentialSubtitle : promptInfo.getSubtitle();
-    }
-
-    @Nullable
-    private static CharSequence getDescription(@NonNull PromptInfo promptInfo) {
-        final CharSequence credentialDescription = promptInfo.getDeviceCredentialDescription();
-        return credentialDescription != null ? credentialDescription : promptInfo.getDescription();
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
index f1e42e0..5c616f0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
@@ -177,11 +177,11 @@
         }
     }
 
-    int getContainerWidth() {
+    public int getContainerWidth() {
         return mContainerWidth;
     }
 
-    int getContainerHeight() {
+    public int getContainerHeight() {
         return mContainerHeight;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 3273d74..48c60a0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -60,6 +60,8 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.doze.DozeReceiver;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -119,6 +121,7 @@
     @NonNull private final SystemUIDialogManager mDialogManager;
     @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @NonNull private final VibratorHelper mVibrator;
+    @NonNull private final FeatureFlags mFeatureFlags;
     @NonNull private final FalsingManager mFalsingManager;
     @NonNull private final PowerManager mPowerManager;
     @NonNull private final AccessibilityManager mAccessibilityManager;
@@ -202,6 +205,10 @@
         @Override
         public void showUdfpsOverlay(long requestId, int sensorId, int reason,
                 @NonNull IUdfpsOverlayControllerCallback callback) {
+            if (mFeatureFlags.isEnabled(Flags.NEW_UDFPS_OVERLAY)) {
+                return;
+            }
+
             mFgExecutor.execute(() -> UdfpsController.this.showUdfpsOverlay(
                     new UdfpsControllerOverlay(mContext, mFingerprintManager, mInflater,
                             mWindowManager, mAccessibilityManager, mStatusBarStateController,
@@ -217,6 +224,10 @@
 
         @Override
         public void hideUdfpsOverlay(int sensorId) {
+            if (mFeatureFlags.isEnabled(Flags.NEW_UDFPS_OVERLAY)) {
+                return;
+            }
+
             mFgExecutor.execute(() -> {
                 if (mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) {
                     // if we get here, we expect keyguardUpdateMonitor's fingerprintRunningState
@@ -590,6 +601,7 @@
             @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             @NonNull DumpManager dumpManager,
             @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor,
+            @NonNull FeatureFlags featureFlags,
             @NonNull FalsingManager falsingManager,
             @NonNull PowerManager powerManager,
             @NonNull AccessibilityManager accessibilityManager,
@@ -625,6 +637,7 @@
         mDumpManager = dumpManager;
         mDialogManager = dialogManager;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+        mFeatureFlags = featureFlags;
         mFalsingManager = falsingManager;
         mPowerManager = powerManager;
         mAccessibilityManager = accessibilityManager;
@@ -859,9 +872,7 @@
             playStartHaptic();
 
             if (!mKeyguardUpdateMonitor.isFaceDetectionRunning()) {
-                mKeyguardUpdateMonitor.requestFaceAuth(
-                        /* userInitiatedRequest */ false,
-                        FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
+                mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
             }
         }
         mOnFingerDown = true;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
index 6e78f3d..142642a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
@@ -19,26 +19,40 @@
 import android.annotation.SuppressLint
 import android.content.Context
 import android.graphics.PixelFormat
+import android.graphics.Point
 import android.graphics.Rect
+import android.hardware.biometrics.BiometricOverlayConstants
 import android.hardware.fingerprint.FingerprintManager
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
 import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback
+import android.hardware.fingerprint.IUdfpsOverlay
 import android.os.Handler
+import android.provider.Settings
 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.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.concurrency.Execution
-import java.util.*
+import java.util.Optional
 import java.util.concurrent.Executor
 import javax.inject.Inject
+import kotlin.math.cos
+import kotlin.math.pow
+import kotlin.math.sin
 
 private const val TAG = "UdfpsOverlay"
 
+const val SETTING_OVERLAY_DEBUG = "udfps_overlay_debug"
+
+// Number of sensor points needed inside ellipse for good overlap
+private const val NEEDED_POINTS = 2
+
 @SuppressLint("ClickableViewAccessibility")
 @SysUISingleton
 class UdfpsOverlay
@@ -51,10 +65,11 @@
     private val handler: Handler,
     private val biometricExecutor: Executor,
     private val alternateTouchProvider: Optional<AlternateUdfpsTouchProvider>,
-    private val fgExecutor: DelayableExecutor,
+    @Main private val fgExecutor: DelayableExecutor,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     private val authController: AuthController,
-    private val udfpsLogger: UdfpsLogger
+    private val udfpsLogger: UdfpsLogger,
+    private var featureFlags: FeatureFlags
 ) : CoreStartable {
 
     /** The view, when [isShowing], or null. */
@@ -64,7 +79,11 @@
     private var requestId: Long = 0
     private var onFingerDown = false
     val size = windowManager.maximumWindowMetrics.bounds
+
     val udfpsProps: MutableList<FingerprintSensorPropertiesInternal> = mutableListOf()
+    var points: Array<Point> = emptyArray()
+    var processedMotionEvent = false
+    var isShowing = false
 
     private var params: UdfpsOverlayParams = UdfpsOverlayParams()
 
@@ -87,41 +106,140 @@
                 inputFeatures = INPUT_FEATURE_SPY
             }
 
-    fun onTouch(v: View, event: MotionEvent): Boolean {
-        val view = v as UdfpsOverlayView
+    fun onTouch(event: MotionEvent): Boolean {
+        val view = overlayView!!
 
         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.processMotionEvent(event)
+
+                    val goodOverlap =
+                        if (featureFlags.isEnabled(Flags.NEW_ELLIPSE_DETECTION)) {
+                            isGoodEllipseOverlap(event)
+                        } else {
+                            isGoodCentroidOverlap(event)
+                        }
+
+                    if (!processedMotionEvent && goodOverlap) {
+                        biometricExecutor.execute {
+                            alternateTouchProvider
+                                .get()
+                                .onPointerDown(
+                                    requestId,
+                                    event.rawX.toInt(),
+                                    event.rawY.toInt(),
+                                    event.touchMinor,
+                                    event.touchMajor
+                                )
+                        }
+                        fgExecutor.execute {
+                            if (keyguardUpdateMonitor.isFingerprintDetectionRunning) {
+                                keyguardUpdateMonitor.onUdfpsPointerDown(requestId.toInt())
+                            }
+
+                            view.configureDisplay {
+                                biometricExecutor.execute {
+                                    alternateTouchProvider.get().onUiReady()
+                                }
+                            }
+
+                            processedMotionEvent = true
                         }
                     }
 
-                    view.configureDisplay {
-                        biometricExecutor.execute { alternateTouchProvider.get().onUiReady() }
-                    }
+                    view.invalidate()
                 }
-
                 true
             }
             MotionEvent.ACTION_UP,
             MotionEvent.ACTION_CANCEL -> {
-                if (onFingerDown && alternateTouchProvider.isPresent) {
+                if (processedMotionEvent && alternateTouchProvider.isPresent) {
+                    biometricExecutor.execute {
+                        alternateTouchProvider.get().onPointerUp(requestId)
+                    }
+                    fgExecutor.execute {
+                        if (keyguardUpdateMonitor.isFingerprintDetectionRunning) {
+                            keyguardUpdateMonitor.onUdfpsPointerUp(requestId.toInt())
+                        }
+                    }
+
+                    processedMotionEvent = false
+                }
+
+                if (view.isDisplayConfigured) {
+                    view.unconfigureDisplay()
+                }
+
+                view.invalidate()
+                true
+            }
+            else -> false
+        }
+    }
+
+    fun isGoodEllipseOverlap(event: MotionEvent): Boolean {
+        return points.count { checkPoint(event, it) } >= NEEDED_POINTS
+    }
+
+    fun isGoodCentroidOverlap(event: MotionEvent): Boolean {
+        return params.sensorBounds.contains(event.rawX.toInt(), event.rawY.toInt())
+    }
+
+    fun checkPoint(event: MotionEvent, point: Point): Boolean {
+        // Calculate if sensor point is within ellipse
+        // Formula: ((cos(o)(xE - xS) + sin(o)(yE - yS))^2 / a^2) + ((sin(o)(xE - xS) + cos(o)(yE -
+        // yS))^2 / b^2) <= 1
+        val a: Float = cos(event.orientation) * (point.x - event.rawX)
+        val b: Float = sin(event.orientation) * (point.y - event.rawY)
+        val c: Float = sin(event.orientation) * (point.x - event.rawX)
+        val d: Float = cos(event.orientation) * (point.y - event.rawY)
+        val result =
+            (a + b).pow(2) / (event.touchMinor / 2).pow(2) +
+                (c - d).pow(2) / (event.touchMajor / 2).pow(2)
+
+        return result <= 1
+    }
+
+    fun show(requestId: Long) {
+        if (!featureFlags.isEnabled(Flags.NEW_UDFPS_OVERLAY)) {
+            return
+        }
+
+        this.requestId = requestId
+        fgExecutor.execute {
+            if (overlayView == null && alternateTouchProvider.isPresent) {
+                UdfpsOverlayView(context, null).let {
+                    it.overlayParams = params
+                    it.setUdfpsDisplayMode(
+                        UdfpsDisplayMode(context, execution, authController, udfpsLogger)
+                    )
+                    it.setOnTouchListener { _, event -> onTouch(event) }
+                    it.sensorPoints = points
+                    it.debugOverlay =
+                        Settings.Global.getInt(
+                            context.contentResolver,
+                            SETTING_OVERLAY_DEBUG,
+                            0 /* def */
+                        ) != 0
+                    overlayView = it
+                }
+                windowManager.addView(overlayView, coreLayoutParams)
+                isShowing = true
+            }
+        }
+    }
+
+    fun hide() {
+        if (!featureFlags.isEnabled(Flags.NEW_UDFPS_OVERLAY)) {
+            return
+        }
+
+        fgExecutor.execute {
+            if (overlayView != null && isShowing && alternateTouchProvider.isPresent) {
+                if (processedMotionEvent) {
                     biometricExecutor.execute {
                         alternateTouchProvider.get().onPointerUp(requestId)
                     }
@@ -131,44 +249,23 @@
                         }
                     }
                 }
-                onFingerDown = false
-                if (view.isDisplayConfigured) {
-                    view.unconfigureDisplay()
+
+                if (overlayView!!.isDisplayConfigured) {
+                    overlayView!!.unconfigureDisplay()
                 }
 
-                true
+                overlayView?.apply {
+                    windowManager.removeView(this)
+                    setOnTouchListener(null)
+                }
+
+                isShowing = false
+                overlayView = null
+                processedMotionEvent = false
             }
-            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(
@@ -180,6 +277,18 @@
                 }
             }
         )
+
+        fingerprintManager?.setUdfpsOverlay(
+            object : IUdfpsOverlay.Stub() {
+                override fun show(
+                    requestId: Long,
+                    sensorId: Int,
+                    @BiometricOverlayConstants.ShowReason reason: Int
+                ) = show(requestId)
+
+                override fun hide(sensorId: Int) = hide()
+            }
+        )
     }
 
     private fun handleAllFingerprintAuthenticatorsRegistered(
@@ -201,6 +310,24 @@
                     naturalDisplayHeight = size.height(),
                     scaleFactor = 1f
                 )
+
+            val sensorX = params.sensorBounds.centerX()
+            val sensorY = params.sensorBounds.centerY()
+            val cornerOffset: Int = params.sensorBounds.width() / 4
+            val sideOffset: Int = params.sensorBounds.width() / 3
+
+            points =
+                arrayOf(
+                    Point(sensorX - cornerOffset, sensorY - cornerOffset),
+                    Point(sensorX, sensorY - sideOffset),
+                    Point(sensorX + cornerOffset, sensorY - cornerOffset),
+                    Point(sensorX - sideOffset, sensorY),
+                    Point(sensorX, sensorY),
+                    Point(sensorX + sideOffset, sensorY),
+                    Point(sensorX - cornerOffset, sensorY + cornerOffset),
+                    Point(sensorX, sensorY + sideOffset),
+                    Point(sensorX + cornerOffset, sensorY + cornerOffset)
+                )
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt
index d371332..4e6a06b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt
@@ -20,26 +20,41 @@
 import android.graphics.Canvas
 import android.graphics.Color
 import android.graphics.Paint
+import android.graphics.Point
 import android.graphics.RectF
 import android.util.AttributeSet
+import android.view.MotionEvent
 import android.widget.FrameLayout
 
 private const val TAG = "UdfpsOverlayView"
+private const val POINT_SIZE = 10f
 
 class UdfpsOverlayView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
-
-    private val sensorRect = RectF()
     var overlayParams = UdfpsOverlayParams()
     private var mUdfpsDisplayMode: UdfpsDisplayMode? = null
 
+    var debugOverlay = false
+
     var overlayPaint = Paint()
     var sensorPaint = Paint()
+    var touchPaint = Paint()
+    var pointPaint = Paint()
     val centerPaint = Paint()
 
+    var oval = RectF()
+
     /** True after the call to [configureDisplay] and before the call to [unconfigureDisplay]. */
     var isDisplayConfigured: Boolean = false
         private set
 
+    var touchX: Float = 0f
+    var touchY: Float = 0f
+    var touchMinor: Float = 0f
+    var touchMajor: Float = 0f
+    var touchOrientation: Double = 0.0
+
+    var sensorPoints: Array<Point>? = null
+
     init {
         this.setWillNotDraw(false)
     }
@@ -47,24 +62,60 @@
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
 
-        overlayPaint.color = Color.argb(120, 255, 0, 0)
+        overlayPaint.color = Color.argb(100, 255, 0, 0)
         overlayPaint.style = Paint.Style.FILL
 
+        touchPaint.color = Color.argb(200, 255, 255, 255)
+        touchPaint.style = Paint.Style.FILL
+
         sensorPaint.color = Color.argb(150, 134, 204, 255)
         sensorPaint.style = Paint.Style.FILL
+
+        pointPaint.color = Color.WHITE
+        pointPaint.style = Paint.Style.FILL
     }
 
     override fun onDraw(canvas: Canvas) {
         super.onDraw(canvas)
 
-        canvas.drawRect(overlayParams.overlayBounds, overlayPaint)
-        canvas.drawRect(overlayParams.sensorBounds, sensorPaint)
+        if (debugOverlay) {
+            // Draw overlay and sensor bounds
+            canvas.drawRect(overlayParams.overlayBounds, overlayPaint)
+            canvas.drawRect(overlayParams.sensorBounds, sensorPaint)
+        }
+
+        // Draw sensor circle
         canvas.drawCircle(
             overlayParams.sensorBounds.exactCenterX(),
             overlayParams.sensorBounds.exactCenterY(),
             overlayParams.sensorBounds.width().toFloat() / 2,
             centerPaint
         )
+
+        if (debugOverlay) {
+            // Draw Points
+            sensorPoints?.forEach {
+                canvas.drawCircle(it.x.toFloat(), it.y.toFloat(), POINT_SIZE, pointPaint)
+            }
+
+            // Draw touch oval
+            canvas.save()
+            canvas.rotate(Math.toDegrees(touchOrientation).toFloat(), touchX, touchY)
+
+            oval.setEmpty()
+            oval.set(
+                touchX - touchMinor / 2,
+                touchY + touchMajor / 2,
+                touchX + touchMinor / 2,
+                touchY - touchMajor / 2
+            )
+
+            canvas.drawOval(oval, touchPaint)
+
+            // Draw center point
+            canvas.drawCircle(touchX, touchY, POINT_SIZE, centerPaint)
+            canvas.restore()
+        }
     }
 
     fun setUdfpsDisplayMode(udfpsDisplayMode: UdfpsDisplayMode?) {
@@ -80,4 +131,12 @@
         isDisplayConfigured = false
         mUdfpsDisplayMode?.disable(null /* onDisabled */)
     }
+
+    fun processMotionEvent(event: MotionEvent) {
+        touchX = event.rawX
+        touchY = event.rawY
+        touchMinor = event.touchMinor
+        touchMajor = event.touchMajor
+        touchOrientation = event.orientation.toDouble()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt
index 75640b7..da50f1c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt
@@ -62,7 +62,6 @@
         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()
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
index b5d81f2..7c0c3b7 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
@@ -16,32 +16,45 @@
 
 package com.android.systemui.biometrics.dagger
 
+import com.android.systemui.biometrics.data.repository.PromptRepository
+import com.android.systemui.biometrics.data.repository.PromptRepositoryImpl
+import com.android.systemui.biometrics.domain.interactor.CredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.CredentialInteractorImpl
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.util.concurrency.ThreadFactory
+import dagger.Binds
 import dagger.Module
 import dagger.Provides
 import java.util.concurrent.Executor
 import javax.inject.Qualifier
 
-/**
- * Dagger module for all things biometric.
- */
+/** Dagger module for all things biometric. */
 @Module
-object BiometricsModule {
+interface BiometricsModule {
 
-    /** Background [Executor] for HAL related operations. */
-    @Provides
+    @Binds
     @SysUISingleton
-    @JvmStatic
-    @BiometricsBackground
-    fun providesPluginExecutor(threadFactory: ThreadFactory): Executor =
-        threadFactory.buildExecutorOnNewThread("biometrics")
+    fun biometricPromptRepository(impl: PromptRepositoryImpl): PromptRepository
+
+    @Binds
+    @SysUISingleton
+    fun providesCredentialInteractor(impl: CredentialInteractorImpl): CredentialInteractor
+
+    companion object {
+        /** Background [Executor] for HAL related operations. */
+        @Provides
+        @SysUISingleton
+        @JvmStatic
+        @BiometricsBackground
+        fun providesPluginExecutor(threadFactory: ThreadFactory): Executor =
+            threadFactory.buildExecutorOnNewThread("biometrics")
+    }
 }
 
 /**
- * Background executor for HAL operations that are latency sensitive but too
- * slow to run on the main thread. Prefer the shared executors, such as
- * [com.android.systemui.dagger.qualifiers.Background] when a HAL is not directly involved.
+ * Background executor for HAL operations that are latency sensitive but too slow to run on the main
+ * thread. Prefer the shared executors, such as [com.android.systemui.dagger.qualifiers.Background]
+ * when a HAL is not directly involved.
  */
 @Qualifier
 @MustBeDocumented
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt
similarity index 64%
copy from packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt
copy to packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt
index 581dafa3..e82646f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt
@@ -14,10 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.systemui.keyguard.domain.model
+package com.android.systemui.biometrics.data.model
 
-/** Enumerates all possible positions for quick affordances that can appear on the lock-screen. */
-enum class KeyguardQuickAffordancePosition {
-    BOTTOM_START,
-    BOTTOM_END,
+import com.android.systemui.biometrics.Utils
+
+// TODO(b/251476085): this should eventually replace Utils.CredentialType
+/** Credential options for biometric prompt. Shadows [Utils.CredentialType]. */
+enum class PromptKind {
+    ANY_BIOMETRIC,
+    PIN,
+    PATTERN,
+    PASSWORD,
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
new file mode 100644
index 0000000..92a13cf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
@@ -0,0 +1,102 @@
+package com.android.systemui.biometrics.data.repository
+
+import android.hardware.biometrics.PromptInfo
+import com.android.systemui.biometrics.AuthController
+import com.android.systemui.biometrics.data.model.PromptKind
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/**
+ * A repository for the global state of BiometricPrompt.
+ *
+ * There is never more than one instance of the prompt at any given time.
+ */
+interface PromptRepository {
+
+    /** If the prompt is showing. */
+    val isShowing: Flow<Boolean>
+
+    /** The app-specific details to show in the prompt. */
+    val promptInfo: StateFlow<PromptInfo?>
+
+    /** The user that the prompt is for. */
+    val userId: StateFlow<Int?>
+
+    /** The gatekeeper challenge, if one is associated with this prompt. */
+    val challenge: StateFlow<Long?>
+
+    /** The kind of credential to use (biometric, pin, pattern, etc.). */
+    val kind: StateFlow<PromptKind>
+
+    /** Update the prompt configuration, which should be set before [isShowing]. */
+    fun setPrompt(
+        promptInfo: PromptInfo,
+        userId: Int,
+        gatekeeperChallenge: Long?,
+        kind: PromptKind = PromptKind.ANY_BIOMETRIC,
+    )
+
+    /** Unset the prompt info. */
+    fun unsetPrompt()
+}
+
+@SysUISingleton
+class PromptRepositoryImpl @Inject constructor(private val authController: AuthController) :
+    PromptRepository {
+
+    override val isShowing: Flow<Boolean> = conflatedCallbackFlow {
+        val callback =
+            object : AuthController.Callback {
+                override fun onBiometricPromptShown() =
+                    trySendWithFailureLogging(true, TAG, "set isShowing")
+
+                override fun onBiometricPromptDismissed() =
+                    trySendWithFailureLogging(false, TAG, "unset isShowing")
+            }
+        authController.addCallback(callback)
+        trySendWithFailureLogging(authController.isShowing, TAG, "update isShowing")
+        awaitClose { authController.removeCallback(callback) }
+    }
+
+    private val _promptInfo: MutableStateFlow<PromptInfo?> = MutableStateFlow(null)
+    override val promptInfo = _promptInfo.asStateFlow()
+
+    private val _challenge: MutableStateFlow<Long?> = MutableStateFlow(null)
+    override val challenge: StateFlow<Long?> = _challenge.asStateFlow()
+
+    private val _userId: MutableStateFlow<Int?> = MutableStateFlow(null)
+    override val userId = _userId.asStateFlow()
+
+    private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.ANY_BIOMETRIC)
+    override val kind = _kind.asStateFlow()
+
+    override fun setPrompt(
+        promptInfo: PromptInfo,
+        userId: Int,
+        gatekeeperChallenge: Long?,
+        kind: PromptKind,
+    ) {
+        _kind.value = kind
+        _userId.value = userId
+        _challenge.value = gatekeeperChallenge
+        _promptInfo.value = promptInfo
+    }
+
+    override fun unsetPrompt() {
+        _promptInfo.value = null
+        _userId.value = null
+        _challenge.value = null
+        _kind.value = PromptKind.ANY_BIOMETRIC
+    }
+
+    companion object {
+        private const val TAG = "BiometricPromptRepository"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
new file mode 100644
index 0000000..1f1a1b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
@@ -0,0 +1,282 @@
+package com.android.systemui.biometrics.domain.interactor
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources
+import android.content.Context
+import android.os.UserManager
+import com.android.internal.widget.LockPatternUtils
+import com.android.internal.widget.LockscreenCredential
+import com.android.internal.widget.VerifyCredentialResponse
+import com.android.systemui.R
+import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.util.time.SystemClock
+import javax.inject.Inject
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+
+/**
+ * A wrapper for [LockPatternUtils] to verify PIN, pattern, or password credentials.
+ *
+ * This class also uses the [DevicePolicyManager] to generate appropriate error messages when policy
+ * exceptions are raised (i.e. wipe device due to excessive failed attempts, etc.).
+ */
+interface CredentialInteractor {
+    /** If the user's pattern credential should be hidden */
+    fun isStealthModeActive(userId: Int): Boolean
+
+    /** Get the effective user id (profile owner, if one exists) */
+    fun getCredentialOwnerOrSelfId(userId: Int): Int
+
+    /**
+     * Verifies a credential and returns a stream of results.
+     *
+     * The final emitted value will either be a [CredentialStatus.Fail.Error] or a
+     * [CredentialStatus.Success.Verified].
+     */
+    fun verifyCredential(
+        request: BiometricPromptRequest.Credential,
+        credential: LockscreenCredential,
+    ): Flow<CredentialStatus>
+}
+
+/** Standard implementation of [CredentialInteractor]. */
+class CredentialInteractorImpl
+@Inject
+constructor(
+    @Application private val applicationContext: Context,
+    private val lockPatternUtils: LockPatternUtils,
+    private val userManager: UserManager,
+    private val devicePolicyManager: DevicePolicyManager,
+    private val systemClock: SystemClock,
+) : CredentialInteractor {
+
+    override fun isStealthModeActive(userId: Int): Boolean =
+        !lockPatternUtils.isVisiblePatternEnabled(userId)
+
+    override fun getCredentialOwnerOrSelfId(userId: Int): Int =
+        userManager.getCredentialOwnerProfile(userId)
+
+    override fun verifyCredential(
+        request: BiometricPromptRequest.Credential,
+        credential: LockscreenCredential,
+    ): Flow<CredentialStatus> = flow {
+        // Request LockSettingsService to return the Gatekeeper Password in the
+        // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the
+        // Gatekeeper Password and operationId.
+        val effectiveUserId = request.userInfo.deviceCredentialOwnerId
+        val response =
+            lockPatternUtils.verifyCredential(
+                credential,
+                effectiveUserId,
+                LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE
+            )
+
+        if (response.isMatched) {
+            lockPatternUtils.userPresent(effectiveUserId)
+
+            // The response passed into this method contains the Gatekeeper
+            // Password. We still have to request Gatekeeper to create a
+            // Hardware Auth Token with the Gatekeeper Password and Challenge
+            // (keystore operationId in this case)
+            val pwHandle = response.gatekeeperPasswordHandle
+            val gkResponse: VerifyCredentialResponse =
+                lockPatternUtils.verifyGatekeeperPasswordHandle(
+                    pwHandle,
+                    request.operationInfo.gatekeeperChallenge,
+                    effectiveUserId
+                )
+            val hat = gkResponse.gatekeeperHAT
+            lockPatternUtils.removeGatekeeperPasswordHandle(pwHandle)
+            emit(CredentialStatus.Success.Verified(hat))
+        } else if (response.timeout > 0) {
+            // if requests are being throttled, update the error message every
+            // second until the temporary lock has expired
+            val deadline: Long =
+                lockPatternUtils.setLockoutAttemptDeadline(effectiveUserId, response.timeout)
+            val interval = LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS
+            var remaining = deadline - systemClock.elapsedRealtime()
+            while (remaining > 0) {
+                emit(
+                    CredentialStatus.Fail.Throttled(
+                        applicationContext.getString(
+                            R.string.biometric_dialog_credential_too_many_attempts,
+                            remaining / 1000
+                        )
+                    )
+                )
+                delay(interval)
+                remaining -= interval
+            }
+            emit(CredentialStatus.Fail.Error(""))
+        } else { // bad request, but not throttled
+            val numAttempts = lockPatternUtils.getCurrentFailedPasswordAttempts(effectiveUserId) + 1
+            val maxAttempts = lockPatternUtils.getMaximumFailedPasswordsForWipe(effectiveUserId)
+            if (maxAttempts <= 0 || numAttempts <= 0) {
+                // use a generic message if there's no maximum number of attempts
+                emit(CredentialStatus.Fail.Error())
+            } else {
+                val remainingAttempts = (maxAttempts - numAttempts).coerceAtLeast(0)
+                emit(
+                    CredentialStatus.Fail.Error(
+                        applicationContext.getString(
+                            R.string.biometric_dialog_credential_attempts_before_wipe,
+                            numAttempts,
+                            maxAttempts
+                        ),
+                        remainingAttempts,
+                        fetchFinalAttemptMessageOrNull(request, remainingAttempts)
+                    )
+                )
+            }
+            lockPatternUtils.reportFailedPasswordAttempt(effectiveUserId)
+        }
+    }
+
+    private fun fetchFinalAttemptMessageOrNull(
+        request: BiometricPromptRequest.Credential,
+        remainingAttempts: Int?,
+    ): String? =
+        if (remainingAttempts != null && remainingAttempts <= 1) {
+            applicationContext.getFinalAttemptMessageOrBlank(
+                request,
+                devicePolicyManager,
+                userManager.getUserTypeForWipe(
+                    devicePolicyManager,
+                    request.userInfo.deviceCredentialOwnerId
+                ),
+                remainingAttempts
+            )
+        } else {
+            null
+        }
+}
+
+private enum class UserType {
+    PRIMARY,
+    MANAGED_PROFILE,
+    SECONDARY,
+}
+
+private fun UserManager.getUserTypeForWipe(
+    devicePolicyManager: DevicePolicyManager,
+    effectiveUserId: Int,
+): UserType {
+    val userToBeWiped =
+        getUserInfo(
+            devicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(effectiveUserId)
+        )
+    return when {
+        userToBeWiped == null || userToBeWiped.isPrimary -> UserType.PRIMARY
+        userToBeWiped.isManagedProfile -> UserType.MANAGED_PROFILE
+        else -> UserType.SECONDARY
+    }
+}
+
+private fun Context.getFinalAttemptMessageOrBlank(
+    request: BiometricPromptRequest.Credential,
+    devicePolicyManager: DevicePolicyManager,
+    userType: UserType,
+    remaining: Int,
+): String =
+    when {
+        remaining == 1 -> getLastAttemptBeforeWipeMessage(request, devicePolicyManager, userType)
+        remaining <= 0 -> getNowWipingMessage(devicePolicyManager, userType)
+        else -> ""
+    }
+
+private fun Context.getLastAttemptBeforeWipeMessage(
+    request: BiometricPromptRequest.Credential,
+    devicePolicyManager: DevicePolicyManager,
+    userType: UserType,
+): String =
+    when (userType) {
+        UserType.PRIMARY -> getLastAttemptBeforeWipeDeviceMessage(request)
+        UserType.MANAGED_PROFILE ->
+            getLastAttemptBeforeWipeProfileMessage(request, devicePolicyManager)
+        UserType.SECONDARY -> getLastAttemptBeforeWipeUserMessage(request)
+    }
+
+private fun Context.getLastAttemptBeforeWipeDeviceMessage(
+    request: BiometricPromptRequest.Credential,
+): String {
+    val id =
+        when (request) {
+            is BiometricPromptRequest.Credential.Pin ->
+                R.string.biometric_dialog_last_pin_attempt_before_wipe_device
+            is BiometricPromptRequest.Credential.Pattern ->
+                R.string.biometric_dialog_last_pattern_attempt_before_wipe_device
+            is BiometricPromptRequest.Credential.Password ->
+                R.string.biometric_dialog_last_password_attempt_before_wipe_device
+        }
+    return getString(id)
+}
+
+private fun Context.getLastAttemptBeforeWipeProfileMessage(
+    request: BiometricPromptRequest.Credential,
+    devicePolicyManager: DevicePolicyManager,
+): String {
+    val id =
+        when (request) {
+            is BiometricPromptRequest.Credential.Pin ->
+                DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PIN_LAST_ATTEMPT
+            is BiometricPromptRequest.Credential.Pattern ->
+                DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PATTERN_LAST_ATTEMPT
+            is BiometricPromptRequest.Credential.Password ->
+                DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT
+        }
+    return devicePolicyManager.resources.getString(id) {
+        // use fallback a string if not found
+        val defaultId =
+            when (request) {
+                is BiometricPromptRequest.Credential.Pin ->
+                    R.string.biometric_dialog_last_pin_attempt_before_wipe_profile
+                is BiometricPromptRequest.Credential.Pattern ->
+                    R.string.biometric_dialog_last_pattern_attempt_before_wipe_profile
+                is BiometricPromptRequest.Credential.Password ->
+                    R.string.biometric_dialog_last_password_attempt_before_wipe_profile
+            }
+        getString(defaultId)
+    }
+}
+
+private fun Context.getLastAttemptBeforeWipeUserMessage(
+    request: BiometricPromptRequest.Credential,
+): String {
+    val resId =
+        when (request) {
+            is BiometricPromptRequest.Credential.Pin ->
+                R.string.biometric_dialog_last_pin_attempt_before_wipe_user
+            is BiometricPromptRequest.Credential.Pattern ->
+                R.string.biometric_dialog_last_pattern_attempt_before_wipe_user
+            is BiometricPromptRequest.Credential.Password ->
+                R.string.biometric_dialog_last_password_attempt_before_wipe_user
+        }
+    return getString(resId)
+}
+
+private fun Context.getNowWipingMessage(
+    devicePolicyManager: DevicePolicyManager,
+    userType: UserType,
+): String {
+    val id =
+        when (userType) {
+            UserType.MANAGED_PROFILE ->
+                DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS
+            else -> DevicePolicyResources.UNDEFINED
+        }
+    return devicePolicyManager.resources.getString(id) {
+        // use fallback a string if not found
+        val defaultId =
+            when (userType) {
+                UserType.PRIMARY ->
+                    com.android.settingslib.R.string.failed_attempts_now_wiping_device
+                UserType.MANAGED_PROFILE ->
+                    com.android.settingslib.R.string.failed_attempts_now_wiping_profile
+                UserType.SECONDARY ->
+                    com.android.settingslib.R.string.failed_attempts_now_wiping_user
+            }
+        getString(defaultId)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialStatus.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialStatus.kt
new file mode 100644
index 0000000..40b7612
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialStatus.kt
@@ -0,0 +1,23 @@
+package com.android.systemui.biometrics.domain.interactor
+
+/** Result of a [CredentialInteractor.verifyCredential] check. */
+sealed interface CredentialStatus {
+    /** A successful result. */
+    sealed interface Success : CredentialStatus {
+        /** The credential is valid and a [hat] has been generated. */
+        data class Verified(val hat: ByteArray) : Success
+    }
+    /** A failed result. */
+    sealed interface Fail : CredentialStatus {
+        val error: String?
+
+        /** The credential check failed with an [error]. */
+        data class Error(
+            override val error: String? = null,
+            val remainingAttempts: Int? = null,
+            val urgentMessage: String? = null,
+        ) : Fail
+        /** The credential check failed with an [error] and is temporarily locked out. */
+        data class Throttled(override val error: String) : Fail
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt
new file mode 100644
index 0000000..6362c2f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt
@@ -0,0 +1,189 @@
+package com.android.systemui.biometrics.domain.interactor
+
+import android.hardware.biometrics.PromptInfo
+import com.android.internal.widget.LockPatternView
+import com.android.internal.widget.LockscreenCredential
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.data.model.PromptKind
+import com.android.systemui.biometrics.data.repository.PromptRepository
+import com.android.systemui.biometrics.domain.model.BiometricOperationInfo
+import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
+import com.android.systemui.biometrics.domain.model.BiometricUserInfo
+import com.android.systemui.dagger.qualifiers.Background
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.lastOrNull
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.withContext
+
+/**
+ * Business logic for BiometricPrompt's CredentialViews, which primarily includes checking a users
+ * PIN, pattern, or password credential instead of a biometric.
+ */
+class BiometricPromptCredentialInteractor
+@Inject
+constructor(
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    private val biometricPromptRepository: PromptRepository,
+    private val credentialInteractor: CredentialInteractor,
+) {
+    /** If the prompt is currently showing. */
+    val isShowing: Flow<Boolean> = biometricPromptRepository.isShowing
+
+    /** Metadata about the current credential prompt, including app-supplied preferences. */
+    val prompt: Flow<BiometricPromptRequest?> =
+        combine(
+                biometricPromptRepository.promptInfo,
+                biometricPromptRepository.challenge,
+                biometricPromptRepository.userId,
+                biometricPromptRepository.kind
+            ) { promptInfo, challenge, userId, kind ->
+                if (promptInfo == null || userId == null || challenge == null) {
+                    return@combine null
+                }
+
+                when (kind) {
+                    PromptKind.PIN ->
+                        BiometricPromptRequest.Credential.Pin(
+                            info = promptInfo,
+                            userInfo = userInfo(userId),
+                            operationInfo = operationInfo(challenge)
+                        )
+                    PromptKind.PATTERN ->
+                        BiometricPromptRequest.Credential.Pattern(
+                            info = promptInfo,
+                            userInfo = userInfo(userId),
+                            operationInfo = operationInfo(challenge),
+                            stealthMode = credentialInteractor.isStealthModeActive(userId)
+                        )
+                    PromptKind.PASSWORD ->
+                        BiometricPromptRequest.Credential.Password(
+                            info = promptInfo,
+                            userInfo = userInfo(userId),
+                            operationInfo = operationInfo(challenge)
+                        )
+                    else -> null
+                }
+            }
+            .distinctUntilChanged()
+
+    private fun userInfo(userId: Int): BiometricUserInfo =
+        BiometricUserInfo(
+            userId = userId,
+            deviceCredentialOwnerId = credentialInteractor.getCredentialOwnerOrSelfId(userId)
+        )
+
+    private fun operationInfo(challenge: Long): BiometricOperationInfo =
+        BiometricOperationInfo(gatekeeperChallenge = challenge)
+
+    /** Most recent error due to [verifyCredential]. */
+    private val _verificationError = MutableStateFlow<CredentialStatus.Fail?>(null)
+    val verificationError: Flow<CredentialStatus.Fail?> = _verificationError.asStateFlow()
+
+    /** Update the current request to use credential-based authentication instead of biometrics. */
+    fun useCredentialsForAuthentication(
+        promptInfo: PromptInfo,
+        @Utils.CredentialType kind: Int,
+        userId: Int,
+        challenge: Long,
+    ) {
+        biometricPromptRepository.setPrompt(
+            promptInfo,
+            userId,
+            challenge,
+            kind.asBiometricPromptCredential()
+        )
+    }
+
+    /** Unset the current authentication request. */
+    fun resetPrompt() {
+        biometricPromptRepository.unsetPrompt()
+    }
+
+    /**
+     * Check a credential and return the attestation token (HAT) if successful.
+     *
+     * This method will not return if credential checks are being throttled until the throttling has
+     * expired and the user can try again. It will periodically update the [verificationError] until
+     * cancelled or the throttling has completed. If the request is not throttled, but unsuccessful,
+     * the [verificationError] will be set and an optional
+     * [CredentialStatus.Fail.Error.urgentMessage] message may be provided to indicate additional
+     * hints to the user (i.e. device will be wiped on next failure, etc.).
+     *
+     * The check happens on the background dispatcher given in the constructor.
+     */
+    suspend fun checkCredential(
+        request: BiometricPromptRequest.Credential,
+        text: CharSequence? = null,
+        pattern: List<LockPatternView.Cell>? = null,
+    ): CredentialStatus =
+        withContext(bgDispatcher) {
+            val credential =
+                when (request) {
+                    is BiometricPromptRequest.Credential.Pin ->
+                        LockscreenCredential.createPinOrNone(text ?: "")
+                    is BiometricPromptRequest.Credential.Password ->
+                        LockscreenCredential.createPasswordOrNone(text ?: "")
+                    is BiometricPromptRequest.Credential.Pattern ->
+                        LockscreenCredential.createPattern(pattern ?: listOf())
+                }
+
+            credential.use { c -> verifyCredential(request, c) }
+        }
+
+    private suspend fun verifyCredential(
+        request: BiometricPromptRequest.Credential,
+        credential: LockscreenCredential?
+    ): CredentialStatus {
+        if (credential == null || credential.isNone) {
+            return CredentialStatus.Fail.Error()
+        }
+
+        val finalStatus =
+            credentialInteractor
+                .verifyCredential(request, credential)
+                .onEach { status ->
+                    when (status) {
+                        is CredentialStatus.Success -> _verificationError.value = null
+                        is CredentialStatus.Fail -> _verificationError.value = status
+                    }
+                }
+                .lastOrNull()
+
+        return finalStatus ?: CredentialStatus.Fail.Error()
+    }
+
+    /**
+     * Report a user-visible error.
+     *
+     * Use this instead of calling [verifyCredential] when it is not necessary because the check
+     * will obviously fail (i.e. too short, empty, etc.)
+     */
+    fun setVerificationError(error: CredentialStatus.Fail.Error?) {
+        if (error != null) {
+            _verificationError.value = error
+        } else {
+            resetVerificationError()
+        }
+    }
+
+    /** Clear the current error message, if any. */
+    fun resetVerificationError() {
+        _verificationError.value = null
+    }
+}
+
+// TODO(b/251476085): remove along with Utils.CredentialType
+/** Convert a [Utils.CredentialType] to the corresponding [PromptKind]. */
+private fun @receiver:Utils.CredentialType Int.asBiometricPromptCredential(): PromptKind =
+    when (this) {
+        Utils.CREDENTIAL_PIN -> PromptKind.PIN
+        Utils.CREDENTIAL_PASSWORD -> PromptKind.PASSWORD
+        Utils.CREDENTIAL_PATTERN -> PromptKind.PATTERN
+        else -> PromptKind.ANY_BIOMETRIC
+    }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricOperationInfo.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricOperationInfo.kt
new file mode 100644
index 0000000..c619b12
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricOperationInfo.kt
@@ -0,0 +1,4 @@
+package com.android.systemui.biometrics.domain.model
+
+/** Metadata about an in-progress biometric operation. */
+data class BiometricOperationInfo(val gatekeeperChallenge: Long = -1)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
new file mode 100644
index 0000000..5ee0381
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
@@ -0,0 +1,69 @@
+package com.android.systemui.biometrics.domain.model
+
+import android.hardware.biometrics.PromptInfo
+
+/**
+ * Preferences for BiometricPrompt, such as title & description, that are immutable while the prompt
+ * is showing.
+ *
+ * This roughly corresponds to a "request" by the system or an app to show BiometricPrompt and it
+ * contains a subset of the information in a [PromptInfo] that is relevant to SysUI.
+ */
+sealed class BiometricPromptRequest(
+    val title: String,
+    val subtitle: String,
+    val description: String,
+    val userInfo: BiometricUserInfo,
+    val operationInfo: BiometricOperationInfo,
+) {
+    /** Prompt using one or more biometrics. */
+    class Biometric(
+        info: PromptInfo,
+        userInfo: BiometricUserInfo,
+        operationInfo: BiometricOperationInfo,
+    ) :
+        BiometricPromptRequest(
+            title = info.title?.toString() ?: "",
+            subtitle = info.subtitle?.toString() ?: "",
+            description = info.description?.toString() ?: "",
+            userInfo = userInfo,
+            operationInfo = operationInfo
+        )
+
+    /** Prompt using a credential (pin, pattern, password). */
+    sealed class Credential(
+        info: PromptInfo,
+        userInfo: BiometricUserInfo,
+        operationInfo: BiometricOperationInfo,
+    ) :
+        BiometricPromptRequest(
+            title = (info.deviceCredentialTitle ?: info.title)?.toString() ?: "",
+            subtitle = (info.deviceCredentialSubtitle ?: info.subtitle)?.toString() ?: "",
+            description = (info.deviceCredentialDescription ?: info.description)?.toString() ?: "",
+            userInfo = userInfo,
+            operationInfo = operationInfo,
+        ) {
+
+        /** PIN prompt. */
+        class Pin(
+            info: PromptInfo,
+            userInfo: BiometricUserInfo,
+            operationInfo: BiometricOperationInfo,
+        ) : Credential(info, userInfo, operationInfo)
+
+        /** Password prompt. */
+        class Password(
+            info: PromptInfo,
+            userInfo: BiometricUserInfo,
+            operationInfo: BiometricOperationInfo,
+        ) : Credential(info, userInfo, operationInfo)
+
+        /** Pattern prompt. */
+        class Pattern(
+            info: PromptInfo,
+            userInfo: BiometricUserInfo,
+            operationInfo: BiometricOperationInfo,
+            val stealthMode: Boolean,
+        ) : Credential(info, userInfo, operationInfo)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricUserInfo.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricUserInfo.kt
new file mode 100644
index 0000000..08da04d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricUserInfo.kt
@@ -0,0 +1,7 @@
+package com.android.systemui.biometrics.domain.model
+
+/** Metadata about the current user BiometricPrompt is being shown to. */
+data class BiometricUserInfo(
+    val userId: Int,
+    val deviceCredentialOwnerId: Int = userId,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
new file mode 100644
index 0000000..bcc0575
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
@@ -0,0 +1,130 @@
+package com.android.systemui.biometrics.ui
+
+import android.content.Context
+import android.content.res.Configuration.ORIENTATION_LANDSCAPE
+import android.text.TextUtils
+import android.util.AttributeSet
+import android.view.View
+import android.view.WindowInsets
+import android.view.WindowInsets.Type.ime
+import android.view.accessibility.AccessibilityManager
+import android.widget.ImageView
+import android.widget.ImeAwareEditText
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.view.isGone
+import com.android.systemui.R
+import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.ui.binder.CredentialViewBinder
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
+
+/** PIN or password credential view for BiometricPrompt. */
+class CredentialPasswordView(context: Context, attrs: AttributeSet?) :
+    LinearLayout(context, attrs), CredentialView, View.OnApplyWindowInsetsListener {
+
+    private lateinit var titleView: TextView
+    private lateinit var subtitleView: TextView
+    private lateinit var descriptionView: TextView
+    private lateinit var iconView: ImageView
+    private lateinit var passwordField: ImeAwareEditText
+    private lateinit var credentialHeader: View
+    private lateinit var credentialInput: View
+
+    private var bottomInset: Int = 0
+
+    private val accessibilityManager by lazy {
+        context.getSystemService(AccessibilityManager::class.java)
+    }
+
+    /** Initializes the view. */
+    override fun init(
+        viewModel: CredentialViewModel,
+        host: CredentialView.Host,
+        panelViewController: AuthPanelController,
+        animatePanel: Boolean,
+    ) {
+        CredentialViewBinder.bind(this, host, viewModel, panelViewController, animatePanel)
+    }
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+
+        titleView = requireViewById(R.id.title)
+        subtitleView = requireViewById(R.id.subtitle)
+        descriptionView = requireViewById(R.id.description)
+        iconView = requireViewById(R.id.icon)
+        subtitleView = requireViewById(R.id.subtitle)
+        passwordField = requireViewById(R.id.lockPassword)
+        credentialHeader = requireViewById(R.id.auth_credential_header)
+        credentialInput = requireViewById(R.id.auth_credential_input)
+
+        setOnApplyWindowInsetsListener(this)
+    }
+
+    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+        super.onLayout(changed, left, top, right, bottom)
+
+        val inputLeftBound: Int
+        val inputTopBound: Int
+        var headerRightBound = right
+        var headerTopBounds = top
+        val subTitleBottom: Int = if (subtitleView.isGone) titleView.bottom else subtitleView.bottom
+        val descBottom = if (descriptionView.isGone) subTitleBottom else descriptionView.bottom
+        if (resources.configuration.orientation == ORIENTATION_LANDSCAPE) {
+            inputTopBound = (bottom - credentialInput.height) / 2
+            inputLeftBound = (right - left) / 2
+            headerRightBound = inputLeftBound
+            headerTopBounds -= iconView.bottom.coerceAtMost(bottomInset)
+        } else {
+            inputTopBound = descBottom + (bottom - descBottom - credentialInput.height) / 2
+            inputLeftBound = (right - left - credentialInput.width) / 2
+        }
+
+        if (descriptionView.bottom > bottomInset) {
+            credentialHeader.layout(left, headerTopBounds, headerRightBound, bottom)
+        }
+        credentialInput.layout(inputLeftBound, inputTopBound, right, bottom)
+    }
+
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+
+        val newWidth = MeasureSpec.getSize(widthMeasureSpec)
+        val newHeight = MeasureSpec.getSize(heightMeasureSpec) - bottomInset
+
+        setMeasuredDimension(newWidth, newHeight)
+
+        val halfWidthSpec = MeasureSpec.makeMeasureSpec(width / 2, MeasureSpec.AT_MOST)
+        val fullHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.UNSPECIFIED)
+        if (resources.configuration.orientation == ORIENTATION_LANDSCAPE) {
+            measureChildren(halfWidthSpec, fullHeightSpec)
+        } else {
+            measureChildren(widthMeasureSpec, fullHeightSpec)
+        }
+    }
+
+    override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets {
+        val bottomInsets = insets.getInsets(ime())
+        if (bottomInset != bottomInsets.bottom) {
+            bottomInset = bottomInsets.bottom
+
+            if (bottomInset > 0 && resources.configuration.orientation == ORIENTATION_LANDSCAPE) {
+                titleView.isSingleLine = true
+                titleView.ellipsize = TextUtils.TruncateAt.MARQUEE
+                titleView.marqueeRepeatLimit = -1
+                // select to enable marquee unless a screen reader is enabled
+                titleView.isSelected = accessibilityManager.shouldMarquee()
+            } else {
+                titleView.isSingleLine = false
+                titleView.ellipsize = null
+                // select to enable marquee unless a screen reader is enabled
+                titleView.isSelected = false
+            }
+
+            requestLayout()
+        }
+        return insets
+    }
+}
+
+private fun AccessibilityManager.shouldMarquee(): Boolean = !isEnabled || !isTouchExplorationEnabled
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt
new file mode 100644
index 0000000..75331f0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt
@@ -0,0 +1,23 @@
+package com.android.systemui.biometrics.ui
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.LinearLayout
+import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.ui.binder.CredentialViewBinder
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
+
+/** Pattern credential view for BiometricPrompt. */
+class CredentialPatternView(context: Context, attrs: AttributeSet?) :
+    LinearLayout(context, attrs), CredentialView {
+
+    /** Initializes the view. */
+    override fun init(
+        viewModel: CredentialViewModel,
+        host: CredentialView.Host,
+        panelViewController: AuthPanelController,
+        animatePanel: Boolean,
+    ) {
+        CredentialViewBinder.bind(this, host, viewModel, panelViewController, animatePanel)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt
new file mode 100644
index 0000000..b7c6a45
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt
@@ -0,0 +1,31 @@
+package com.android.systemui.biometrics.ui
+
+import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
+
+/** A credential variant of BiometricPrompt. */
+sealed interface CredentialView {
+    /**
+     * Callbacks for the "host" container view that contains this credential view.
+     *
+     * TODO(b/251476085): Removed when the host view is converted to use a parent view model.
+     */
+    interface Host {
+        /** When the user's credential has been verified. */
+        fun onCredentialMatched(attestation: ByteArray)
+
+        /** When the user abandons credential verification. */
+        fun onCredentialAborted()
+
+        /** Warn the user is warned about excessive attempts. */
+        fun onCredentialAttemptsRemaining(remaining: Int, messageBody: String)
+    }
+
+    // TODO(251476085): remove AuthPanelController
+    fun init(
+        viewModel: CredentialViewModel,
+        host: Host,
+        panelViewController: AuthPanelController,
+        animatePanel: Boolean,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt
new file mode 100644
index 0000000..c619648
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt
@@ -0,0 +1,104 @@
+package com.android.systemui.biometrics.ui.binder
+
+import android.view.KeyEvent
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.InputMethodManager
+import android.widget.ImeAwareEditText
+import android.widget.TextView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
+import com.android.systemui.biometrics.ui.CredentialPasswordView
+import com.android.systemui.biometrics.ui.CredentialView
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+/** Sub-binder for the [CredentialPasswordView]. */
+object CredentialPasswordViewBinder {
+
+    /** Bind the view. */
+    fun bind(
+        view: CredentialPasswordView,
+        host: CredentialView.Host,
+        viewModel: CredentialViewModel,
+    ) {
+        val imeManager = view.context.getSystemService(InputMethodManager::class.java)!!
+
+        val passwordField: ImeAwareEditText = view.requireViewById(R.id.lockPassword)
+
+        view.repeatWhenAttached {
+            passwordField.requestFocus()
+            passwordField.scheduleShowSoftInput()
+
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                // observe credential validation attempts and submit/cancel buttons
+                launch {
+                    viewModel.header.collect { header ->
+                        passwordField.setTextOperationUser(header.user)
+                        passwordField.setOnEditorActionListener(
+                            OnImeSubmitListener { text ->
+                                launch { viewModel.checkCredential(text, header) }
+                            }
+                        )
+                        passwordField.setOnKeyListener(
+                            OnBackButtonListener { host.onCredentialAborted() }
+                        )
+                    }
+                }
+
+                launch {
+                    viewModel.inputFlags.collect { flags ->
+                        flags?.let { passwordField.inputType = it }
+                    }
+                }
+
+                // dismiss on a valid credential check
+                launch {
+                    viewModel.validatedAttestation.collect { attestation ->
+                        if (attestation != null) {
+                            imeManager.hideSoftInputFromWindow(view.windowToken, 0 /* flags */)
+                            host.onCredentialMatched(attestation)
+                        } else {
+                            passwordField.setText("")
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+private class OnBackButtonListener(private val onBack: () -> Unit) : View.OnKeyListener {
+    override fun onKey(v: View, keyCode: Int, event: KeyEvent): Boolean {
+        if (keyCode != KeyEvent.KEYCODE_BACK) {
+            return false
+        }
+        if (event.action == KeyEvent.ACTION_UP) {
+            onBack()
+        }
+        return true
+    }
+}
+
+private class OnImeSubmitListener(private val onSubmit: (text: CharSequence) -> Unit) :
+    TextView.OnEditorActionListener {
+    override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent?): Boolean {
+        val isSoftImeEvent =
+            event == null &&
+                (actionId == EditorInfo.IME_NULL ||
+                    actionId == EditorInfo.IME_ACTION_DONE ||
+                    actionId == EditorInfo.IME_ACTION_NEXT)
+        val isKeyboardEnterKey =
+            event != null &&
+                KeyEvent.isConfirmKey(event.keyCode) &&
+                event.action == KeyEvent.ACTION_DOWN
+        if (isSoftImeEvent || isKeyboardEnterKey) {
+            onSubmit(v.text)
+            return true
+        }
+        return false
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt
new file mode 100644
index 0000000..4765551
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt
@@ -0,0 +1,75 @@
+package com.android.systemui.biometrics.ui.binder
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.internal.widget.LockPatternUtils
+import com.android.internal.widget.LockPatternView
+import com.android.systemui.R
+import com.android.systemui.biometrics.ui.CredentialPatternView
+import com.android.systemui.biometrics.ui.CredentialView
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+/** Sub-binder for the [CredentialPatternView]. */
+object CredentialPatternViewBinder {
+
+    /** Bind the view. */
+    fun bind(
+        view: CredentialPatternView,
+        host: CredentialView.Host,
+        viewModel: CredentialViewModel,
+    ) {
+        val lockPatternView: LockPatternView = view.requireViewById(R.id.lockPattern)
+
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                // observe credential validation attempts and submit/cancel buttons
+                launch {
+                    viewModel.header.collect { header ->
+                        lockPatternView.setOnPatternListener(
+                            OnPatternDetectedListener { pattern ->
+                                if (pattern.isPatternLongEnough()) {
+                                    // Pattern size is less than the minimum
+                                    // do not count it as a failed attempt
+                                    viewModel.showPatternTooShortError()
+                                } else {
+                                    lockPatternView.isEnabled = false
+                                    launch { viewModel.checkCredential(pattern, header) }
+                                }
+                            }
+                        )
+                    }
+                }
+
+                launch { viewModel.stealthMode.collect { lockPatternView.isInStealthMode = it } }
+
+                // dismiss on a valid credential check
+                launch {
+                    viewModel.validatedAttestation.collect { attestation ->
+                        val matched = attestation != null
+                        lockPatternView.isEnabled = !matched
+                        if (matched) {
+                            host.onCredentialMatched(attestation!!)
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+private class OnPatternDetectedListener(
+    private val onDetected: (pattern: List<LockPatternView.Cell>) -> Unit
+) : LockPatternView.OnPatternListener {
+    override fun onPatternCellAdded(pattern: List<LockPatternView.Cell>) {}
+    override fun onPatternCleared() {}
+    override fun onPatternStart() {}
+    override fun onPatternDetected(pattern: List<LockPatternView.Cell>) {
+        onDetected(pattern)
+    }
+}
+
+private fun List<LockPatternView.Cell>.isPatternLongEnough(): Boolean =
+    size < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
new file mode 100644
index 0000000..fcc9487
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
@@ -0,0 +1,140 @@
+package com.android.systemui.biometrics.ui.binder
+
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.biometrics.AuthDialog
+import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.ui.CredentialPasswordView
+import com.android.systemui.biometrics.ui.CredentialPatternView
+import com.android.systemui.biometrics.ui.CredentialView
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+/**
+ * View binder for all credential variants of BiometricPrompt, including [CredentialPatternView] and
+ * [CredentialPasswordView].
+ *
+ * This binder delegates to sub-binders for each variant, such as the [CredentialPasswordViewBinder]
+ * and [CredentialPatternViewBinder].
+ */
+object CredentialViewBinder {
+
+    /** Binds a [CredentialPasswordView] or [CredentialPatternView] to a [CredentialViewModel]. */
+    @JvmStatic
+    fun bind(
+        view: ViewGroup,
+        host: CredentialView.Host,
+        viewModel: CredentialViewModel,
+        panelViewController: AuthPanelController,
+        animatePanel: Boolean,
+        maxErrorDuration: Long = 3_000L,
+    ) {
+        val titleView: TextView = view.requireViewById(R.id.title)
+        val subtitleView: TextView = view.requireViewById(R.id.subtitle)
+        val descriptionView: TextView = view.requireViewById(R.id.description)
+        val iconView: ImageView? = view.findViewById(R.id.icon)
+        val errorView: TextView = view.requireViewById(R.id.error)
+
+        var errorTimer: Job? = null
+
+        // bind common elements
+        view.repeatWhenAttached {
+            if (animatePanel) {
+                with(panelViewController) {
+                    // Credential view is always full screen.
+                    setUseFullScreen(true)
+                    updateForContentDimensions(
+                        containerWidth,
+                        containerHeight,
+                        0 /* animateDurationMs */
+                    )
+                }
+            }
+
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                // show prompt metadata
+                launch {
+                    viewModel.header.collect { header ->
+                        titleView.text = header.title
+                        view.announceForAccessibility(header.title)
+
+                        subtitleView.textOrHide = header.subtitle
+                        descriptionView.textOrHide = header.description
+
+                        iconView?.setImageDrawable(header.icon)
+
+                        // Only animate this if we're transitioning from a biometric view.
+                        if (viewModel.animateContents.value) {
+                            view.animateCredentialViewIn()
+                        }
+                    }
+                }
+
+                // show transient error messages
+                launch {
+                    viewModel.errorMessage
+                        .onEach { msg ->
+                            errorTimer?.cancel()
+                            if (msg.isNotBlank()) {
+                                errorTimer = launch {
+                                    delay(maxErrorDuration)
+                                    viewModel.resetErrorMessage()
+                                }
+                            }
+                        }
+                        .collect { errorView.textOrHide = it }
+                }
+
+                // show an extra dialog if the remaining attempts becomes low
+                launch {
+                    viewModel.remainingAttempts
+                        .filter { it.remaining != null }
+                        .collect { info ->
+                            host.onCredentialAttemptsRemaining(info.remaining!!, info.message)
+                        }
+                }
+            }
+        }
+
+        // bind the auth widget
+        when (view) {
+            is CredentialPasswordView -> CredentialPasswordViewBinder.bind(view, host, viewModel)
+            is CredentialPatternView -> CredentialPatternViewBinder.bind(view, host, viewModel)
+            else -> throw IllegalStateException("unexpected view type: ${view.javaClass.name}")
+        }
+    }
+}
+
+private fun View.animateCredentialViewIn() {
+    translationY = resources.getDimension(R.dimen.biometric_dialog_credential_translation_offset)
+    alpha = 0f
+    postOnAnimation {
+        animate()
+            .translationY(0f)
+            .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS.toLong())
+            .alpha(1f)
+            .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
+            .withLayer()
+            .start()
+    }
+}
+
+private var TextView.textOrHide: String?
+    set(value) {
+        val gone = value.isNullOrBlank()
+        visibility = if (gone) View.GONE else View.VISIBLE
+        text = if (gone) "" else value
+    }
+    get() = text?.toString()
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
new file mode 100644
index 0000000..84bbceb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
@@ -0,0 +1,178 @@
+package com.android.systemui.biometrics.ui.viewmodel
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+import android.text.InputType
+import com.android.internal.widget.LockPatternView
+import com.android.systemui.R
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.CredentialStatus
+import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlin.reflect.KClass
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.map
+
+/** View-model for all CredentialViews within BiometricPrompt. */
+class CredentialViewModel
+@Inject
+constructor(
+    @Application private val applicationContext: Context,
+    private val credentialInteractor: BiometricPromptCredentialInteractor,
+) {
+
+    /** Top level information about the prompt. */
+    val header: Flow<HeaderViewModel> =
+        credentialInteractor.prompt.filterIsInstance<BiometricPromptRequest.Credential>().map {
+            request ->
+            BiometricPromptHeaderViewModelImpl(
+                request,
+                user = UserHandle.of(request.userInfo.userId),
+                title = request.title,
+                subtitle = request.subtitle,
+                description = request.description,
+                icon = applicationContext.asLockIcon(request.userInfo.deviceCredentialOwnerId),
+            )
+        }
+
+    /** Input flags for text based credential views */
+    val inputFlags: Flow<Int?> =
+        credentialInteractor.prompt.map {
+            when (it) {
+                is BiometricPromptRequest.Credential.Pin ->
+                    InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
+                else -> null
+            }
+        }
+
+    /** If stealth mode is active (hide user credential input). */
+    val stealthMode: Flow<Boolean> =
+        credentialInteractor.prompt.map {
+            when (it) {
+                is BiometricPromptRequest.Credential.Pattern -> it.stealthMode
+                else -> false
+            }
+        }
+
+    private val _animateContents: MutableStateFlow<Boolean> = MutableStateFlow(true)
+    /** If this view should be animated on transitions. */
+    val animateContents = _animateContents.asStateFlow()
+
+    /** Error messages to show the user. */
+    val errorMessage: Flow<String> =
+        combine(credentialInteractor.verificationError, credentialInteractor.prompt) { error, p ->
+            when (error) {
+                is CredentialStatus.Fail.Error -> error.error
+                        ?: applicationContext.asBadCredentialErrorMessage(p)
+                is CredentialStatus.Fail.Throttled -> error.error
+                null -> ""
+            }
+        }
+
+    private val _validatedAttestation: MutableSharedFlow<ByteArray?> = MutableSharedFlow()
+    /** Results of [checkPatternCredential]. A non-null attestation is supplied on success. */
+    val validatedAttestation: Flow<ByteArray?> = _validatedAttestation.asSharedFlow()
+
+    private val _remainingAttempts: MutableStateFlow<RemainingAttempts> =
+        MutableStateFlow(RemainingAttempts())
+    /** If set, the number of remaining attempts before the user must stop. */
+    val remainingAttempts: Flow<RemainingAttempts> = _remainingAttempts.asStateFlow()
+
+    /** Enable transition animations. */
+    fun setAnimateContents(animate: Boolean) {
+        _animateContents.value = animate
+    }
+
+    /** Show an error message to inform the user the pattern is too short to attempt validation. */
+    fun showPatternTooShortError() {
+        credentialInteractor.setVerificationError(
+            CredentialStatus.Fail.Error(
+                applicationContext.asBadCredentialErrorMessage(
+                    BiometricPromptRequest.Credential.Pattern::class
+                )
+            )
+        )
+    }
+
+    /** Reset the error message to an empty string. */
+    fun resetErrorMessage() {
+        credentialInteractor.resetVerificationError()
+    }
+
+    /** Check a PIN or password and update [validatedAttestation] or [remainingAttempts]. */
+    suspend fun checkCredential(text: CharSequence, header: HeaderViewModel) =
+        checkCredential(credentialInteractor.checkCredential(header.asRequest(), text = text))
+
+    /** Check a pattern and update [validatedAttestation] or [remainingAttempts]. */
+    suspend fun checkCredential(pattern: List<LockPatternView.Cell>, header: HeaderViewModel) =
+        checkCredential(credentialInteractor.checkCredential(header.asRequest(), pattern = pattern))
+
+    private suspend fun checkCredential(result: CredentialStatus) {
+        when (result) {
+            is CredentialStatus.Success.Verified -> {
+                _validatedAttestation.emit(result.hat)
+                _remainingAttempts.value = RemainingAttempts()
+            }
+            is CredentialStatus.Fail.Error -> {
+                _validatedAttestation.emit(null)
+                _remainingAttempts.value =
+                    RemainingAttempts(result.remainingAttempts, result.urgentMessage ?: "")
+            }
+            is CredentialStatus.Fail.Throttled -> {
+                // required for completeness, but a throttled error cannot be the final result
+                _validatedAttestation.emit(null)
+                _remainingAttempts.value = RemainingAttempts()
+            }
+        }
+    }
+}
+
+private fun Context.asBadCredentialErrorMessage(prompt: BiometricPromptRequest?): String =
+    asBadCredentialErrorMessage(
+        if (prompt != null) prompt::class else BiometricPromptRequest.Credential.Password::class
+    )
+
+private fun <T : BiometricPromptRequest> Context.asBadCredentialErrorMessage(
+    clazz: KClass<T>
+): String =
+    getString(
+        when (clazz) {
+            BiometricPromptRequest.Credential.Pin::class -> R.string.biometric_dialog_wrong_pin
+            BiometricPromptRequest.Credential.Password::class ->
+                R.string.biometric_dialog_wrong_password
+            BiometricPromptRequest.Credential.Pattern::class ->
+                R.string.biometric_dialog_wrong_pattern
+            else -> R.string.biometric_dialog_wrong_password
+        }
+    )
+
+private fun Context.asLockIcon(userId: Int): Drawable {
+    val id =
+        if (Utils.isManagedProfile(this, userId)) {
+            R.drawable.auth_dialog_enterprise
+        } else {
+            R.drawable.auth_dialog_lock
+        }
+    return resources.getDrawable(id, theme)
+}
+
+private class BiometricPromptHeaderViewModelImpl(
+    val request: BiometricPromptRequest.Credential,
+    override val user: UserHandle,
+    override val title: String,
+    override val subtitle: String,
+    override val description: String,
+    override val icon: Drawable,
+) : HeaderViewModel
+
+private fun HeaderViewModel.asRequest(): BiometricPromptRequest.Credential =
+    (this as BiometricPromptHeaderViewModelImpl).request
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt
new file mode 100644
index 0000000..ba23f1c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt
@@ -0,0 +1,13 @@
+package com.android.systemui.biometrics.ui.viewmodel
+
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+
+/** View model for the top-level header / info area of BiometricPrompt. */
+interface HeaderViewModel {
+    val user: UserHandle
+    val title: String
+    val subtitle: String
+    val description: String
+    val icon: Drawable
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/RemainingAttempts.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/RemainingAttempts.kt
new file mode 100644
index 0000000..0f22173
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/RemainingAttempts.kt
@@ -0,0 +1,4 @@
+package com.android.systemui.biometrics.ui.viewmodel
+
+/** Metadata about the number of credential attempts the user has left [remaining], if known. */
+data class RemainingAttempts(val remaining: Int? = null, val message: String = "")
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index e47e636..6db56210 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -84,6 +84,7 @@
 import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule;
 import com.android.systemui.statusbar.window.StatusBarWindowModule;
 import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule;
+import com.android.systemui.temporarydisplay.dagger.TemporaryDisplayModule;
 import com.android.systemui.tuner.dagger.TunerModule;
 import com.android.systemui.unfold.SysUIUnfoldModule;
 import com.android.systemui.user.UserModule;
@@ -150,6 +151,7 @@
             SysUIConcurrencyModule.class,
             SysUIUnfoldModule.class,
             TelephonyRepositoryModule.class,
+            TemporaryDisplayModule.class,
             TunerModule.class,
             UserModule.class,
             UtilModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
index 3adeeac..1f061e9 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
@@ -21,6 +21,7 @@
 import static com.android.systemui.flags.FlagManager.EXTRA_FLAGS;
 import static com.android.systemui.flags.FlagManager.EXTRA_ID;
 import static com.android.systemui.flags.FlagManager.EXTRA_VALUE;
+import static com.android.systemui.flags.FlagsCommonModule.ALL_FLAGS;
 
 import static java.util.Objects.requireNonNull;
 
@@ -59,20 +60,20 @@
  *
  * Flags can be set (or unset) via the following adb command:
  *
- *   adb shell cmd statusbar flag <id> <on|off|toggle|erase>
+ * adb shell cmd statusbar flag <id> <on|off|toggle|erase>
  *
- *  Alternatively, you can change flags via a broadcast intent:
+ * Alternatively, you can change flags via a broadcast intent:
  *
- *   adb shell am broadcast -a com.android.systemui.action.SET_FLAG --ei id <id> [--ez value <0|1>]
+ * adb shell am broadcast -a com.android.systemui.action.SET_FLAG --ei id <id> [--ez value <0|1>]
  *
  * To restore a flag back to its default, leave the `--ez value <0|1>` off of the command.
  */
 @SysUISingleton
 public class FeatureFlagsDebug implements FeatureFlags {
     static final String TAG = "SysUIFlags";
-    static final String ALL_FLAGS = "all_flags";
 
     private final FlagManager mFlagManager;
+    private final Context mContext;
     private final SecureSettings mSecureSettings;
     private final Resources mResources;
     private final SystemPropertiesHelper mSystemProperties;
@@ -83,6 +84,14 @@
     private final Map<Integer, String> mStringFlagCache = new TreeMap<>();
     private final Restarter mRestarter;
 
+    private final ServerFlagReader.ChangeListener mOnPropertiesChanged =
+            new ServerFlagReader.ChangeListener() {
+                @Override
+                public void onChange() {
+                    mRestarter.restart();
+                }
+            };
+
     @Inject
     public FeatureFlagsDebug(
             FlagManager flagManager,
@@ -93,23 +102,28 @@
             DeviceConfigProxy deviceConfigProxy,
             ServerFlagReader serverFlagReader,
             @Named(ALL_FLAGS) Map<Integer, Flag<?>> allFlags,
-            Restarter barService) {
+            Restarter restarter) {
         mFlagManager = flagManager;
+        mContext = context;
         mSecureSettings = secureSettings;
         mResources = resources;
         mSystemProperties = systemProperties;
         mDeviceConfigProxy = deviceConfigProxy;
         mServerFlagReader = serverFlagReader;
         mAllFlags = allFlags;
-        mRestarter = barService;
+        mRestarter = restarter;
+    }
 
+    /** Call after construction to setup listeners. */
+    void init() {
         IntentFilter filter = new IntentFilter();
         filter.addAction(ACTION_SET_FLAG);
         filter.addAction(ACTION_GET_FLAGS);
-        flagManager.setOnSettingsChangedAction(this::restartSystemUI);
-        flagManager.setClearCacheAction(this::removeFromCache);
-        context.registerReceiver(mReceiver, filter, null, null,
+        mFlagManager.setOnSettingsChangedAction(this::restartSystemUI);
+        mFlagManager.setClearCacheAction(this::removeFromCache);
+        mContext.registerReceiver(mReceiver, filter, null, null,
                 Context.RECEIVER_EXPORTED_UNAUDITED);
+        mServerFlagReader.listenForChanges(mAllFlags.values(), mOnPropertiesChanged);
     }
 
     @Override
@@ -196,7 +210,7 @@
         return mStringFlagCache.get(id);
     }
 
-    /** Specific override for Boolean flags that checks against the teamfood list.*/
+    /** Specific override for Boolean flags that checks against the teamfood list. */
     private boolean readFlagValue(int id, boolean defaultValue) {
         Boolean result = readBooleanFlagOverride(id);
         boolean hasServerOverride = mServerFlagReader.hasOverride(id);
@@ -273,6 +287,7 @@
     private void dispatchListenersAndMaybeRestart(int id, Consumer<Boolean> restartAction) {
         mFlagManager.dispatchListenersAndMaybeRestart(id, restartAction);
     }
+
     /** Works just like {@link #eraseFlag(int)} except that it doesn't restart SystemUI. */
     private void eraseInternal(int id) {
         // We can't actually "erase" things from sysprops, but we can set them to empty!
@@ -358,7 +373,7 @@
                     }
                 }
 
-                Bundle extras =  getResultExtras(true);
+                Bundle extras = getResultExtras(true);
                 if (extras != null) {
                     extras.putParcelableArrayList(EXTRA_FLAGS, pFlags);
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
index 560dcbd..6271334 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt
@@ -31,7 +31,7 @@
     dumpManager: DumpManager,
     private val commandRegistry: CommandRegistry,
     private val flagCommand: FlagCommand,
-    featureFlags: FeatureFlags
+    private val featureFlags: FeatureFlagsDebug
 ) : CoreStartable {
 
     init {
@@ -41,6 +41,7 @@
     }
 
     override fun start() {
+        featureFlags.init()
         commandRegistry.registerCommand(FlagCommand.FLAG_COMMAND) { flagCommand }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
index 40a8a1a..30cad5f 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.flags;
 
+import static com.android.systemui.flags.FlagsCommonModule.ALL_FLAGS;
+
 import static java.util.Objects.requireNonNull;
 
 import android.content.res.Resources;
@@ -34,6 +36,7 @@
 import java.util.Map;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 
 /**
  * Default implementation of the a Flag manager that returns default values for release builds
@@ -49,26 +52,47 @@
     private final SystemPropertiesHelper mSystemProperties;
     private final DeviceConfigProxy mDeviceConfigProxy;
     private final ServerFlagReader mServerFlagReader;
+    private final Restarter mRestarter;
+    private final Map<Integer, Flag<?>> mAllFlags;
     SparseBooleanArray mBooleanCache = new SparseBooleanArray();
     SparseArray<String> mStringCache = new SparseArray<>();
 
+    private final ServerFlagReader.ChangeListener mOnPropertiesChanged =
+            new ServerFlagReader.ChangeListener() {
+                @Override
+                public void onChange() {
+                    mRestarter.restart();
+                }
+            };
+
     @Inject
     public FeatureFlagsRelease(
             @Main Resources resources,
             SystemPropertiesHelper systemProperties,
             DeviceConfigProxy deviceConfigProxy,
-            ServerFlagReader serverFlagReader) {
+            ServerFlagReader serverFlagReader,
+            @Named(ALL_FLAGS) Map<Integer, Flag<?>> allFlags,
+            Restarter restarter) {
         mResources = resources;
         mSystemProperties = systemProperties;
         mDeviceConfigProxy = deviceConfigProxy;
         mServerFlagReader = serverFlagReader;
+        mAllFlags = allFlags;
+        mRestarter = restarter;
+    }
+
+    /** Call after construction to setup listeners. */
+    void init() {
+        mServerFlagReader.listenForChanges(mAllFlags.values(), mOnPropertiesChanged);
     }
 
     @Override
-    public void addListener(@NonNull Flag<?> flag, @NonNull Listener listener) {}
+    public void addListener(@NonNull Flag<?> flag, @NonNull Listener listener) {
+    }
 
     @Override
-    public void removeListener(@NonNull Listener listener) {}
+    public void removeListener(@NonNull Listener listener) {
+    }
 
     @Override
     public boolean isEnabled(@NotNull UnreleasedFlag flag) {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java b/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java
index 4d25431..1e93c0b7 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.flags;
 
+import static com.android.systemui.flags.FlagsCommonModule.ALL_FLAGS;
+
 import androidx.annotation.NonNull;
 
 import com.android.systemui.statusbar.commandline.Command;
@@ -42,7 +44,7 @@
     @Inject
     FlagCommand(
             FeatureFlagsDebug featureFlags,
-            @Named(FeatureFlagsDebug.ALL_FLAGS) Map<Integer, Flag<?>> allFlags
+            @Named(ALL_FLAGS) Map<Integer, Flag<?>> allFlags
     ) {
         mFeatureFlags = featureFlags;
         mAllFlags = allFlags;
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 4818bcc..fa49968 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -124,6 +124,10 @@
      */
     @JvmField val DOZING_MIGRATION_1 = UnreleasedFlag(213, teamfood = true)
 
+    @JvmField val NEW_ELLIPSE_DETECTION = UnreleasedFlag(214)
+
+    @JvmField val NEW_UDFPS_OVERLAY = UnreleasedFlag(215)
+
     // 300 - power menu
     // TODO(b/254512600): Tracking Bug
     @JvmField val POWER_MENU_LITE = ReleasedFlag(300)
@@ -196,7 +200,7 @@
 
     // 802 - wallpaper rendering
     // TODO(b/254512923): Tracking Bug
-    @JvmField val USE_CANVAS_RENDERER = ReleasedFlag(802)
+    @JvmField val USE_CANVAS_RENDERER = UnreleasedFlag(802, teamfood = true)
 
     // 803 - screen contents translation
     // TODO(b/254513187): Tracking Bug
@@ -230,15 +234,9 @@
     // 1000 - dock
     val SIMULATE_DOCK_THROUGH_CHARGING = ReleasedFlag(1000)
 
-    // TODO(b/254512444): Tracking Bug
-    @JvmField val DOCK_SETUP_ENABLED = ReleasedFlag(1001)
-
     // TODO(b/254512758): Tracking Bug
     @JvmField val ROUNDED_BOX_RIPPLE = ReleasedFlag(1002)
 
-    // TODO(b/254512525): Tracking Bug
-    @JvmField val REFACTORED_DOCK_SETUP = ReleasedFlag(1003, teamfood = true)
-
     // 1100 - windowing
     @Keep
     val WM_ENABLE_SHELL_TRANSITIONS =
@@ -320,7 +318,7 @@
 
     // 1500 - chooser
     // TODO(b/254512507): Tracking Bug
-    val CHOOSER_UNBUNDLED = UnreleasedFlag(1500)
+    val CHOOSER_UNBUNDLED = UnreleasedFlag(1500, true)
 
     // 1600 - accessibility
     @JvmField val A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS = UnreleasedFlag(1600)
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt
new file mode 100644
index 0000000..e1f4944
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.flags
+
+import com.android.internal.statusbar.IStatusBarService
+import dagger.Module
+import dagger.Provides
+import javax.inject.Named
+
+/** Module containing shared code for all FeatureFlag implementations. */
+@Module
+interface FlagsCommonModule {
+    companion object {
+        const val ALL_FLAGS = "all_flags"
+
+        @JvmStatic
+        @Provides
+        @Named(ALL_FLAGS)
+        fun providesAllFlags(): Map<Int, Flag<*>> {
+            return Flags.collectFlags()
+        }
+
+        @JvmStatic
+        @Provides
+        fun providesRestarter(barService: IStatusBarService): Restarter {
+            return object : Restarter {
+                override fun restart() {
+                    barService.restart()
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt b/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt
index fc5b9f4..694fa01 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt
@@ -16,9 +16,13 @@
 
 package com.android.systemui.flags
 
+import android.provider.DeviceConfig
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.util.DeviceConfigProxy
-import dagger.Binds
 import dagger.Module
+import dagger.Provides
+import java.util.concurrent.Executor
 import javax.inject.Inject
 
 interface ServerFlagReader {
@@ -27,40 +31,99 @@
 
     /** Returns any stored server-side setting or the default if not set. */
     fun readServerOverride(flagId: Int, default: Boolean): Boolean
+
+    /** Register a listener for changes to any of the passed in flags. */
+    fun listenForChanges(values: Collection<Flag<*>>, listener: ChangeListener)
+
+    interface ChangeListener {
+        fun onChange()
+    }
 }
 
 class ServerFlagReaderImpl @Inject constructor(
-    private val deviceConfig: DeviceConfigProxy
+    private val namespace: String,
+    private val deviceConfig: DeviceConfigProxy,
+    @Background private val executor: Executor
 ) : ServerFlagReader {
+
+    private val listeners =
+        mutableListOf<Pair<ServerFlagReader.ChangeListener, Collection<Flag<*>>>>()
+
+    private val onPropertiesChangedListener = object : DeviceConfig.OnPropertiesChangedListener {
+        override fun onPropertiesChanged(properties: DeviceConfig.Properties) {
+            if (properties.namespace != namespace) {
+                return
+            }
+
+            for ((listener, flags) in listeners) {
+                propLoop@ for (propName in properties.keyset) {
+                    for (flag in flags) {
+                        if (propName == getServerOverrideName(flag.id)) {
+                            listener.onChange()
+                            break@propLoop
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     override fun hasOverride(flagId: Int): Boolean =
         deviceConfig.getProperty(
-            SYSUI_NAMESPACE,
+            namespace,
             getServerOverrideName(flagId)
         ) != null
 
     override fun readServerOverride(flagId: Int, default: Boolean): Boolean {
         return deviceConfig.getBoolean(
-            SYSUI_NAMESPACE,
+            namespace,
             getServerOverrideName(flagId),
             default
         )
     }
 
+    override fun listenForChanges(
+        flags: Collection<Flag<*>>,
+        listener: ServerFlagReader.ChangeListener
+    ) {
+        if (listeners.isEmpty()) {
+            deviceConfig.addOnPropertiesChangedListener(
+                namespace,
+                executor,
+                onPropertiesChangedListener
+            )
+        }
+        listeners.add(Pair(listener, flags))
+    }
+
     private fun getServerOverrideName(flagId: Int): String {
         return "flag_override_$flagId"
     }
 }
 
-private val SYSUI_NAMESPACE = "systemui"
-
 @Module
 interface ServerFlagReaderModule {
-    @Binds
-    fun bindsReader(impl: ServerFlagReaderImpl): ServerFlagReader
+    companion object {
+        private val SYSUI_NAMESPACE = "systemui"
+
+        @JvmStatic
+        @Provides
+        @SysUISingleton
+        fun bindsReader(
+            deviceConfig: DeviceConfigProxy,
+            @Background executor: Executor
+        ): ServerFlagReader {
+            return ServerFlagReaderImpl(
+                SYSUI_NAMESPACE, deviceConfig, executor
+            )
+        }
+    }
 }
 
 class ServerFlagReaderFake : ServerFlagReader {
     private val flagMap: MutableMap<Int, Boolean> = mutableMapOf()
+    private val listeners =
+        mutableListOf<Pair<ServerFlagReader.ChangeListener, Collection<Flag<*>>>>()
 
     override fun hasOverride(flagId: Int): Boolean {
         return flagMap.containsKey(flagId)
@@ -72,9 +135,24 @@
 
     fun setFlagValue(flagId: Int, value: Boolean) {
         flagMap.put(flagId, value)
+
+        for ((listener, flags) in listeners) {
+            flagLoop@ for (flag in flags) {
+                if (flagId == flag.id) {
+                    listener.onChange()
+                    break@flagLoop
+                }
+            }
+        }
     }
 
     fun eraseFlag(flagId: Int) {
         flagMap.remove(flagId)
     }
+
+    override fun listenForChanges(
+        flags: Collection<Flag<*>>,
+        listener: ServerFlagReader.ChangeListener
+    ) {
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
new file mode 100644
index 0000000..a069582
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.keyguard.data.quickaffordance
+
+/**
+ * Unique identifier keys for all known built-in quick affordances.
+ *
+ * Please ensure uniqueness by never associating more than one class with each key.
+ */
+object BuiltInKeyguardQuickAffordanceKeys {
+    // Please keep alphabetical order of const names to simplify future maintenance.
+    const val HOME_CONTROLS = "home"
+    const val QR_CODE_SCANNER = "qr_code_scanner"
+    const val QUICK_ACCESS_WALLET = "wallet"
+    // Please keep alphabetical order of const names to simplify future maintenance.
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
similarity index 75%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
index 8384260..c600e13 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
@@ -1,21 +1,21 @@
 /*
- *  Copyright (C) 2022 The Android Open Source Project
+ * 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
+ * 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
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
  *
  */
 
-package com.android.systemui.keyguard.domain.quickaffordance
+package com.android.systemui.keyguard.data.quickaffordance
 
 import android.content.Context
 import android.content.Intent
@@ -51,19 +51,21 @@
 
     private val appContext = context.applicationContext
 
-    override val state: Flow<KeyguardQuickAffordanceConfig.State> =
+    override val key: String = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
+
+    override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
         component.canShowWhileLockedSetting.flatMapLatest { canShowWhileLocked ->
             if (canShowWhileLocked) {
                 stateInternal(component.getControlsListingController().getOrNull())
             } else {
-                flowOf(KeyguardQuickAffordanceConfig.State.Hidden)
+                flowOf(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
             }
         }
 
-    override fun onQuickAffordanceClicked(
+    override fun onTriggered(
         expandable: Expandable?,
-    ): KeyguardQuickAffordanceConfig.OnClickedResult {
-        return KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity(
+    ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
+        return KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity(
             intent =
                 Intent(appContext, ControlsActivity::class.java)
                     .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
@@ -77,9 +79,9 @@
 
     private fun stateInternal(
         listingController: ControlsListingController?,
-    ): Flow<KeyguardQuickAffordanceConfig.State> {
+    ): Flow<KeyguardQuickAffordanceConfig.LockScreenState> {
         if (listingController == null) {
-            return flowOf(KeyguardQuickAffordanceConfig.State.Hidden)
+            return flowOf(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
         }
 
         return conflatedCallbackFlow {
@@ -114,7 +116,7 @@
         hasServiceInfos: Boolean,
         visibility: ControlsComponent.Visibility,
         @DrawableRes iconResourceId: Int?,
-    ): KeyguardQuickAffordanceConfig.State {
+    ): KeyguardQuickAffordanceConfig.LockScreenState {
         return if (
             isFeatureEnabled &&
                 hasFavorites &&
@@ -122,7 +124,7 @@
                 iconResourceId != null &&
                 visibility == ControlsComponent.Visibility.AVAILABLE
         ) {
-            KeyguardQuickAffordanceConfig.State.Visible(
+            KeyguardQuickAffordanceConfig.LockScreenState.Visible(
                 icon =
                     Icon.Resource(
                         res = iconResourceId,
@@ -133,7 +135,7 @@
                     ),
             )
         } else {
-            KeyguardQuickAffordanceConfig.State.Hidden
+            KeyguardQuickAffordanceConfig.LockScreenState.Hidden
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
new file mode 100644
index 0000000..0a8090b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.keyguard.data.quickaffordance
+
+import android.content.Intent
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
+import kotlinx.coroutines.flow.Flow
+
+/** Defines interface that can act as data source for a single quick affordance model. */
+interface KeyguardQuickAffordanceConfig {
+
+    /** Unique identifier for this quick affordance. It must be globally unique. */
+    val key: String
+
+    /**
+     * The ever-changing state of the affordance.
+     *
+     * Used to populate the lock screen.
+     */
+    val lockScreenState: Flow<LockScreenState>
+
+    /**
+     * Notifies that the affordance was clicked by the user.
+     *
+     * @param expandable An [Expandable] to use when animating dialogs or activities
+     * @return An [OnTriggeredResult] telling the caller what to do next
+     */
+    fun onTriggered(expandable: Expandable?): OnTriggeredResult
+
+    /**
+     * Encapsulates the state of a "quick affordance" in the keyguard bottom area (for example, a
+     * button on the lock-screen).
+     */
+    sealed class LockScreenState {
+
+        /** No affordance should show up. */
+        object Hidden : LockScreenState()
+
+        /** An affordance is visible. */
+        data class Visible(
+            /** An icon for the affordance. */
+            val icon: Icon,
+            /** The activation state of the affordance. */
+            val activationState: ActivationState = ActivationState.NotSupported,
+        ) : LockScreenState()
+    }
+
+    sealed class OnTriggeredResult {
+        /**
+         * Returning this as a result from the [onTriggered] method means that the implementation
+         * has taken care of the action, the system will do nothing.
+         */
+        object Handled : OnTriggeredResult()
+
+        /**
+         * Returning this as a result from the [onTriggered] method means that the implementation
+         * has _not_ taken care of the action and the system should start an activity using the
+         * given [Intent].
+         */
+        data class StartActivity(
+            val intent: Intent,
+            val canShowWhileLocked: Boolean,
+        ) : OnTriggeredResult()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
new file mode 100644
index 0000000..d620b2a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.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.systemui.keyguard.data.quickaffordance
+
+import com.android.systemui.R
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qrcodescanner.controller.QRCodeScannerController
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** QR code scanner quick affordance data source. */
+@SysUISingleton
+class QrCodeScannerKeyguardQuickAffordanceConfig
+@Inject
+constructor(
+    private val controller: QRCodeScannerController,
+) : KeyguardQuickAffordanceConfig {
+
+    override val key: String = BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER
+
+    override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
+        conflatedCallbackFlow {
+            val callback =
+                object : QRCodeScannerController.Callback {
+                    override fun onQRCodeScannerActivityChanged() {
+                        trySendWithFailureLogging(state(), TAG)
+                    }
+                    override fun onQRCodeScannerPreferenceChanged() {
+                        trySendWithFailureLogging(state(), TAG)
+                    }
+                }
+
+            controller.addCallback(callback)
+            controller.registerQRCodeScannerChangeObservers(
+                QRCodeScannerController.DEFAULT_QR_CODE_SCANNER_CHANGE,
+                QRCodeScannerController.QR_CODE_SCANNER_PREFERENCE_CHANGE
+            )
+            // Registering does not push an initial update.
+            trySendWithFailureLogging(state(), "initial state", TAG)
+
+            awaitClose {
+                controller.unregisterQRCodeScannerChangeObservers(
+                    QRCodeScannerController.DEFAULT_QR_CODE_SCANNER_CHANGE,
+                    QRCodeScannerController.QR_CODE_SCANNER_PREFERENCE_CHANGE
+                )
+                controller.removeCallback(callback)
+            }
+        }
+
+    override fun onTriggered(
+        expandable: Expandable?,
+    ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
+        return KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity(
+            intent = controller.intent,
+            canShowWhileLocked = true,
+        )
+    }
+
+    private fun state(): KeyguardQuickAffordanceConfig.LockScreenState {
+        return if (controller.isEnabledForLockScreenButton) {
+            KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                icon =
+                    Icon.Resource(
+                        res = R.drawable.ic_qr_code_scanner,
+                        contentDescription =
+                            ContentDescription.Resource(
+                                res = R.string.accessibility_qr_code_scanner_button,
+                            ),
+                    ),
+            )
+        } else {
+            KeyguardQuickAffordanceConfig.LockScreenState.Hidden
+        }
+    }
+
+    companion object {
+        private const val TAG = "QrCodeScannerKeyguardQuickAffordanceConfig"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
new file mode 100644
index 0000000..be57a32
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.keyguard.data.quickaffordance
+
+import android.graphics.drawable.Drawable
+import android.service.quickaccesswallet.GetWalletCardsError
+import android.service.quickaccesswallet.GetWalletCardsResponse
+import android.service.quickaccesswallet.QuickAccessWalletClient
+import android.util.Log
+import com.android.systemui.R
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.wallet.controller.QuickAccessWalletController
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** Quick access wallet quick affordance data source. */
+@SysUISingleton
+class QuickAccessWalletKeyguardQuickAffordanceConfig
+@Inject
+constructor(
+    private val walletController: QuickAccessWalletController,
+    private val activityStarter: ActivityStarter,
+) : KeyguardQuickAffordanceConfig {
+
+    override val key: String = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
+
+    override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
+        conflatedCallbackFlow {
+            val callback =
+                object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
+                    override fun onWalletCardsRetrieved(response: GetWalletCardsResponse?) {
+                        trySendWithFailureLogging(
+                            state(
+                                isFeatureEnabled = walletController.isWalletEnabled,
+                                hasCard = response?.walletCards?.isNotEmpty() == true,
+                                tileIcon = walletController.walletClient.tileIcon,
+                            ),
+                            TAG,
+                        )
+                    }
+
+                    override fun onWalletCardRetrievalError(error: GetWalletCardsError?) {
+                        Log.e(TAG, "Wallet card retrieval error, message: \"${error?.message}\"")
+                        trySendWithFailureLogging(
+                            KeyguardQuickAffordanceConfig.LockScreenState.Hidden,
+                            TAG,
+                        )
+                    }
+                }
+
+            walletController.setupWalletChangeObservers(
+                callback,
+                QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE,
+                QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE
+            )
+            walletController.updateWalletPreference()
+            walletController.queryWalletCards(callback)
+
+            awaitClose {
+                walletController.unregisterWalletChangeObservers(
+                    QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE,
+                    QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE
+                )
+            }
+        }
+
+    override fun onTriggered(
+        expandable: Expandable?,
+    ): KeyguardQuickAffordanceConfig.OnTriggeredResult {
+        walletController.startQuickAccessUiIntent(
+            activityStarter,
+            expandable?.activityLaunchController(),
+            /* hasCard= */ true,
+        )
+        return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
+    }
+
+    private fun state(
+        isFeatureEnabled: Boolean,
+        hasCard: Boolean,
+        tileIcon: Drawable?,
+    ): KeyguardQuickAffordanceConfig.LockScreenState {
+        return if (isFeatureEnabled && hasCard && tileIcon != null) {
+            KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+                icon =
+                    Icon.Loaded(
+                        drawable = tileIcon,
+                        contentDescription =
+                            ContentDescription.Resource(
+                                res = R.string.accessibility_wallet_button,
+                            ),
+                    ),
+            )
+        } else {
+            KeyguardQuickAffordanceConfig.LockScreenState.Hidden
+        }
+    }
+
+    companion object {
+        private const val TAG = "QuickAccessWalletKeyguardQuickAffordanceConfig"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index f663b0d..13d97aa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -21,15 +21,14 @@
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
 import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
-import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
 import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceRegistry
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import javax.inject.Inject
-import kotlin.reflect.KClass
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.onStart
@@ -63,25 +62,25 @@
     }
 
     /**
-     * Notifies that a quick affordance has been clicked by the user.
+     * Notifies that a quick affordance has been "triggered" (clicked) by the user.
      *
      * @param configKey The configuration key corresponding to the [KeyguardQuickAffordanceModel] of
      * the affordance that was clicked
      * @param expandable An optional [Expandable] for the activity- or dialog-launch animation
      */
-    fun onQuickAffordanceClicked(
-        configKey: KClass<out KeyguardQuickAffordanceConfig>,
+    fun onQuickAffordanceTriggered(
+        configKey: String,
         expandable: Expandable?,
     ) {
-        @Suppress("UNCHECKED_CAST") val config = registry.get(configKey as KClass<Nothing>)
-        when (val result = config.onQuickAffordanceClicked(expandable)) {
-            is KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity ->
+        @Suppress("UNCHECKED_CAST") val config = registry.get(configKey)
+        when (val result = config.onTriggered(expandable)) {
+            is KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity ->
                 launchQuickAffordance(
                     intent = result.intent,
                     canShowWhileLocked = result.canShowWhileLocked,
                     expandable = expandable,
                 )
-            is KeyguardQuickAffordanceConfig.OnClickedResult.Handled -> Unit
+            is KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled -> Unit
         }
     }
 
@@ -95,16 +94,20 @@
                 // value and avoid subtle bugs where the downstream isn't receiving any values
                 // because one config implementation is not emitting an initial value. For example,
                 // see b/244296596.
-                config.state.onStart { emit(KeyguardQuickAffordanceConfig.State.Hidden) }
+                config.lockScreenState.onStart {
+                    emit(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
+                }
             }
         ) { states ->
-            val index = states.indexOfFirst { it is KeyguardQuickAffordanceConfig.State.Visible }
+            val index =
+                states.indexOfFirst { it is KeyguardQuickAffordanceConfig.LockScreenState.Visible }
             if (index != -1) {
-                val visibleState = states[index] as KeyguardQuickAffordanceConfig.State.Visible
+                val visibleState =
+                    states[index] as KeyguardQuickAffordanceConfig.LockScreenState.Visible
                 KeyguardQuickAffordanceModel.Visible(
-                    configKey = configs[index]::class,
+                    configKey = configs[index].key,
                     icon = visibleState.icon,
-                    toggle = visibleState.toggle,
+                    activationState = visibleState.activationState,
                 )
             } else {
                 KeyguardQuickAffordanceModel.Hidden
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt
index e56b259..32560af 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt
@@ -18,9 +18,7 @@
 package com.android.systemui.keyguard.domain.model
 
 import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
-import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
-import kotlin.reflect.KClass
+import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
 
 /**
  * Models a "quick affordance" in the keyguard bottom area (for example, a button on the
@@ -33,10 +31,10 @@
     /** A affordance is visible. */
     data class Visible(
         /** Identifier for the affordance this is modeling. */
-        val configKey: KClass<out KeyguardQuickAffordanceConfig>,
+        val configKey: String,
         /** An icon for the affordance. */
         val icon: Icon,
-        /** The toggle state for the affordance. */
-        val toggle: KeyguardQuickAffordanceToggleState,
+        /** The activation state of the affordance. */
+        val activationState: ActivationState,
     ) : KeyguardQuickAffordanceModel()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt
deleted file mode 100644
index 95027d0..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt
+++ /dev/null
@@ -1,69 +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.keyguard.domain.quickaffordance
-
-import android.content.Intent
-import com.android.systemui.animation.Expandable
-import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
-import kotlinx.coroutines.flow.Flow
-
-/** Defines interface that can act as data source for a single quick affordance model. */
-interface KeyguardQuickAffordanceConfig {
-
-    val state: Flow<State>
-
-    fun onQuickAffordanceClicked(expandable: Expandable?): OnClickedResult
-
-    /**
-     * Encapsulates the state of a "quick affordance" in the keyguard bottom area (for example, a
-     * button on the lock-screen).
-     */
-    sealed class State {
-
-        /** No affordance should show up. */
-        object Hidden : State()
-
-        /** An affordance is visible. */
-        data class Visible(
-            /** An icon for the affordance. */
-            val icon: Icon,
-            /** The toggle state for the affordance. */
-            val toggle: KeyguardQuickAffordanceToggleState =
-                KeyguardQuickAffordanceToggleState.NotSupported,
-        ) : State()
-    }
-
-    sealed class OnClickedResult {
-        /**
-         * Returning this as a result from the [onQuickAffordanceClicked] method means that the
-         * implementation has taken care of the click, the system will do nothing.
-         */
-        object Handled : OnClickedResult()
-
-        /**
-         * Returning this as a result from the [onQuickAffordanceClicked] method means that the
-         * implementation has _not_ taken care of the click and the system should start an activity
-         * using the given [Intent].
-         */
-        data class StartActivity(
-            val intent: Intent,
-            val canShowWhileLocked: Boolean,
-        ) : OnClickedResult()
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceModule.kt
index 94024d4..b48acb6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceModule.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.keyguard.domain.quickaffordance
 
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
 import dagger.Binds
 import dagger.Module
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceRegistry.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceRegistry.kt
index ad40ee7..8526ada 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceRegistry.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceRegistry.kt
@@ -17,14 +17,17 @@
 
 package com.android.systemui.keyguard.domain.quickaffordance
 
-import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
+import com.android.systemui.keyguard.data.quickaffordance.HomeControlsKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.QrCodeScannerKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.QuickAccessWalletKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 import javax.inject.Inject
-import kotlin.reflect.KClass
 
 /** Central registry of all known quick affordance configs. */
 interface KeyguardQuickAffordanceRegistry<T : KeyguardQuickAffordanceConfig> {
     fun getAll(position: KeyguardQuickAffordancePosition): List<T>
-    fun get(configClass: KClass<out T>): T
+    fun get(key: String): T
 }
 
 class KeyguardQuickAffordanceRegistryImpl
@@ -46,8 +49,8 @@
                     qrCodeScanner,
                 ),
         )
-    private val configByClass =
-        configsByPosition.values.flatten().associateBy { config -> config::class }
+    private val configByKey =
+        configsByPosition.values.flatten().associateBy { config -> config.key }
 
     override fun getAll(
         position: KeyguardQuickAffordancePosition,
@@ -56,8 +59,8 @@
     }
 
     override fun get(
-        configClass: KClass<out KeyguardQuickAffordanceConfig>
+        key: String,
     ): KeyguardQuickAffordanceConfig {
-        return configByClass.getValue(configClass)
+        return configByKey.getValue(key)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
deleted file mode 100644
index 502a607..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
+++ /dev/null
@@ -1,97 +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.keyguard.domain.quickaffordance
-
-import com.android.systemui.R
-import com.android.systemui.animation.Expandable
-import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.common.shared.model.ContentDescription
-import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qrcodescanner.controller.QRCodeScannerController
-import javax.inject.Inject
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-
-/** QR code scanner quick affordance data source. */
-@SysUISingleton
-class QrCodeScannerKeyguardQuickAffordanceConfig
-@Inject
-constructor(
-    private val controller: QRCodeScannerController,
-) : KeyguardQuickAffordanceConfig {
-
-    override val state: Flow<KeyguardQuickAffordanceConfig.State> = conflatedCallbackFlow {
-        val callback =
-            object : QRCodeScannerController.Callback {
-                override fun onQRCodeScannerActivityChanged() {
-                    trySendWithFailureLogging(state(), TAG)
-                }
-                override fun onQRCodeScannerPreferenceChanged() {
-                    trySendWithFailureLogging(state(), TAG)
-                }
-            }
-
-        controller.addCallback(callback)
-        controller.registerQRCodeScannerChangeObservers(
-            QRCodeScannerController.DEFAULT_QR_CODE_SCANNER_CHANGE,
-            QRCodeScannerController.QR_CODE_SCANNER_PREFERENCE_CHANGE
-        )
-        // Registering does not push an initial update.
-        trySendWithFailureLogging(state(), "initial state", TAG)
-
-        awaitClose {
-            controller.unregisterQRCodeScannerChangeObservers(
-                QRCodeScannerController.DEFAULT_QR_CODE_SCANNER_CHANGE,
-                QRCodeScannerController.QR_CODE_SCANNER_PREFERENCE_CHANGE
-            )
-            controller.removeCallback(callback)
-        }
-    }
-
-    override fun onQuickAffordanceClicked(
-        expandable: Expandable?,
-    ): KeyguardQuickAffordanceConfig.OnClickedResult {
-        return KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity(
-            intent = controller.intent,
-            canShowWhileLocked = true,
-        )
-    }
-
-    private fun state(): KeyguardQuickAffordanceConfig.State {
-        return if (controller.isEnabledForLockScreenButton) {
-            KeyguardQuickAffordanceConfig.State.Visible(
-                icon =
-                    Icon.Resource(
-                        res = R.drawable.ic_qr_code_scanner,
-                        contentDescription =
-                            ContentDescription.Resource(
-                                res = R.string.accessibility_qr_code_scanner_button,
-                            ),
-                    ),
-            )
-        } else {
-            KeyguardQuickAffordanceConfig.State.Hidden
-        }
-    }
-
-    companion object {
-        private const val TAG = "QrCodeScannerKeyguardQuickAffordanceConfig"
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
deleted file mode 100644
index a24a0d6..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
+++ /dev/null
@@ -1,121 +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.keyguard.domain.quickaffordance
-
-import android.graphics.drawable.Drawable
-import android.service.quickaccesswallet.GetWalletCardsError
-import android.service.quickaccesswallet.GetWalletCardsResponse
-import android.service.quickaccesswallet.QuickAccessWalletClient
-import android.util.Log
-import com.android.systemui.R
-import com.android.systemui.animation.Expandable
-import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.common.shared.model.ContentDescription
-import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.wallet.controller.QuickAccessWalletController
-import javax.inject.Inject
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-
-/** Quick access wallet quick affordance data source. */
-@SysUISingleton
-class QuickAccessWalletKeyguardQuickAffordanceConfig
-@Inject
-constructor(
-    private val walletController: QuickAccessWalletController,
-    private val activityStarter: ActivityStarter,
-) : KeyguardQuickAffordanceConfig {
-
-    override val state: Flow<KeyguardQuickAffordanceConfig.State> = conflatedCallbackFlow {
-        val callback =
-            object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
-                override fun onWalletCardsRetrieved(response: GetWalletCardsResponse?) {
-                    trySendWithFailureLogging(
-                        state(
-                            isFeatureEnabled = walletController.isWalletEnabled,
-                            hasCard = response?.walletCards?.isNotEmpty() == true,
-                            tileIcon = walletController.walletClient.tileIcon,
-                        ),
-                        TAG,
-                    )
-                }
-
-                override fun onWalletCardRetrievalError(error: GetWalletCardsError?) {
-                    Log.e(TAG, "Wallet card retrieval error, message: \"${error?.message}\"")
-                    trySendWithFailureLogging(
-                        KeyguardQuickAffordanceConfig.State.Hidden,
-                        TAG,
-                    )
-                }
-            }
-
-        walletController.setupWalletChangeObservers(
-            callback,
-            QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE,
-            QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE
-        )
-        walletController.updateWalletPreference()
-        walletController.queryWalletCards(callback)
-
-        awaitClose {
-            walletController.unregisterWalletChangeObservers(
-                QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE,
-                QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE
-            )
-        }
-    }
-
-    override fun onQuickAffordanceClicked(
-        expandable: Expandable?,
-    ): KeyguardQuickAffordanceConfig.OnClickedResult {
-        walletController.startQuickAccessUiIntent(
-            activityStarter,
-            expandable?.activityLaunchController(),
-            /* hasCard= */ true,
-        )
-        return KeyguardQuickAffordanceConfig.OnClickedResult.Handled
-    }
-
-    private fun state(
-        isFeatureEnabled: Boolean,
-        hasCard: Boolean,
-        tileIcon: Drawable?,
-    ): KeyguardQuickAffordanceConfig.State {
-        return if (isFeatureEnabled && hasCard && tileIcon != null) {
-            KeyguardQuickAffordanceConfig.State.Visible(
-                icon =
-                    Icon.Loaded(
-                        drawable = tileIcon,
-                        contentDescription =
-                            ContentDescription.Resource(
-                                res = R.string.accessibility_wallet_button,
-                            ),
-                    ),
-            )
-        } else {
-            KeyguardQuickAffordanceConfig.State.Hidden
-        }
-    }
-
-    companion object {
-        private const val TAG = "QuickAccessWalletKeyguardQuickAffordanceConfig"
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordanceToggleState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/ActivationState.kt
similarity index 68%
rename from packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordanceToggleState.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/ActivationState.kt
index 55d38a4..a68d190 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordanceToggleState.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/ActivationState.kt
@@ -17,12 +17,12 @@
 
 package com.android.systemui.keyguard.shared.quickaffordance
 
-/** Enumerates all possible toggle states for a quick affordance on the lock-screen. */
-sealed class KeyguardQuickAffordanceToggleState {
-    /** Toggling is not supported. */
-    object NotSupported : KeyguardQuickAffordanceToggleState()
+/** Enumerates all possible activation states for a quick affordance on the lock-screen. */
+sealed class ActivationState {
+    /** Activation is not supported. */
+    object NotSupported : ActivationState()
     /** The quick affordance is on. */
-    object On : KeyguardQuickAffordanceToggleState()
+    object Active : ActivationState()
     /** The quick affordance is off. */
-    object Off : KeyguardQuickAffordanceToggleState()
+    object Inactive : ActivationState()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt
similarity index 92%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt
index 581dafa3..a18b036 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.keyguard.domain.model
+package com.android.systemui.keyguard.shared.quickaffordance
 
 /** Enumerates all possible positions for quick affordances that can appear on the lock-screen. */
 enum class KeyguardQuickAffordancePosition {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
index 535ca72..b6b2304 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
@@ -22,8 +22,8 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
 import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
-import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
-import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
+import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
@@ -118,13 +118,13 @@
                     animateReveal = animateReveal,
                     icon = icon,
                     onClicked = { parameters ->
-                        quickAffordanceInteractor.onQuickAffordanceClicked(
+                        quickAffordanceInteractor.onQuickAffordanceTriggered(
                             configKey = parameters.configKey,
                             expandable = parameters.expandable,
                         )
                     },
                     isClickable = isClickable,
-                    isActivated = toggle is KeyguardQuickAffordanceToggleState.On,
+                    isActivated = activationState is ActivationState.Active,
                 )
             is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
index bf598ba..44f48f9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
@@ -18,12 +18,10 @@
 
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
-import kotlin.reflect.KClass
 
 /** Models the UI state of a keyguard quick affordance button. */
 data class KeyguardQuickAffordanceViewModel(
-    val configKey: KClass<out KeyguardQuickAffordanceConfig>? = null,
+    val configKey: String? = null,
     val isVisible: Boolean = false,
     /** Whether to animate the transition of the quick affordance from invisible to visible. */
     val animateReveal: Boolean = false,
@@ -33,7 +31,7 @@
     val isActivated: Boolean = false,
 ) {
     data class OnClickedParameters(
-        val configKey: KClass<out KeyguardQuickAffordanceConfig>,
+        val configKey: String,
         val expandable: Expandable?,
     )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
index 6b46d8f..cbb670e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
@@ -43,7 +43,8 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.media.dream.MediaDreamComplication
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.shade.NotifPanelEvents
+import com.android.systemui.shade.ShadeStateEvents
+import com.android.systemui.shade.ShadeStateEvents.ShadeStateEventsListener
 import com.android.systemui.statusbar.CrossFadeHelper
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -96,7 +97,7 @@
     private val dreamOverlayStateController: DreamOverlayStateController,
     configurationController: ConfigurationController,
     wakefulnessLifecycle: WakefulnessLifecycle,
-    panelEventsEvents: NotifPanelEvents,
+    panelEventsEvents: ShadeStateEvents,
     private val secureSettings: SecureSettings,
     @Main private val handler: Handler,
 ) {
@@ -534,8 +535,8 @@
             mediaHosts.forEach { it?.updateViewVisibility() }
         }
 
-        panelEventsEvents.registerListener(
-            object : NotifPanelEvents.Listener {
+        panelEventsEvents.addShadeStateEventsListener(
+            object : ShadeStateEventsListener {
                 override fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) {
                     skipQqsOnExpansion = isExpandImmediateEnabled
                     updateDesiredLocation()
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
index 0a60437..769494a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
@@ -27,10 +27,11 @@
 /** Utility methods for media tap-to-transfer. */
 class MediaTttUtils {
     companion object {
-        // Used in CTS tests UpdateMediaTapToTransferSenderDisplayTest and
-        // UpdateMediaTapToTransferReceiverDisplayTest
-        const val WINDOW_TITLE = "Media Transfer Chip View"
-        const val WAKE_REASON = "MEDIA_TRANSFER_ACTIVATED"
+        const val WINDOW_TITLE_SENDER = "Media Transfer Chip View (Sender)"
+        const val WINDOW_TITLE_RECEIVER = "Media Transfer Chip View (Receiver)"
+
+        const val WAKE_REASON_SENDER = "MEDIA_TRANSFER_ACTIVATED_SENDER"
+        const val WAKE_REASON_RECEIVER = "MEDIA_TRANSFER_ACTIVATED_RECEIVER"
 
         /**
          * Returns the information needed to display the icon in [Icon] form.
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index dc794e6..7dd9fb4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -40,7 +40,6 @@
 import com.android.systemui.media.taptotransfer.common.MediaTttUtils
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS
 import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
 import com.android.systemui.temporarydisplay.TemporaryViewInfo
 import com.android.systemui.util.animation.AnimationUtil.Companion.frames
@@ -78,8 +77,6 @@
         configurationController,
         powerManager,
         R.layout.media_ttt_chip_receiver,
-        MediaTttUtils.WINDOW_TITLE,
-        MediaTttUtils.WAKE_REASON,
 ) {
     @SuppressLint("WrongConstant") // We're allowed to use LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
     override val windowLayoutParams = commonWindowLayoutParams.apply {
@@ -231,7 +228,7 @@
 data class ChipReceiverInfo(
     val routeInfo: MediaRoute2Info,
     val appIconDrawableOverride: Drawable?,
-    val appNameOverride: CharSequence?
-) : TemporaryViewInfo {
-    override fun getTimeoutMs() = DEFAULT_TIMEOUT_MILLIS
-}
+    val appNameOverride: CharSequence?,
+    override val windowTitle: String = MediaTttUtils.WINDOW_TITLE_RECEIVER,
+    override val wakeReason: String = MediaTttUtils.WAKE_REASON_RECEIVER,
+) : TemporaryViewInfo()
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
index 6e596ee..af7317c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
@@ -43,7 +43,7 @@
     @StringRes val stringResId: Int?,
     val transferStatus: TransferStatus,
     val endItem: SenderEndItem?,
-    val timeout: Long = DEFAULT_TIMEOUT_MILLIS
+    val timeout: Int = DEFAULT_TIMEOUT_MILLIS,
 ) {
     /**
      * A state representing that the two devices are close but not close enough to *start* a cast to
@@ -223,6 +223,6 @@
 // Give the Transfer*Triggered states a longer timeout since those states represent an active
 // process and we should keep the user informed about it as long as possible (but don't allow it to
 // continue indefinitely).
-private const val TRANSFER_TRIGGERED_TIMEOUT_MILLIS = 30000L
+private const val TRANSFER_TRIGGERED_TIMEOUT_MILLIS = 30000
 
 private const val TAG = "ChipStateSender"
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
index 1fa8fae..d1ea2d0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
@@ -159,6 +159,9 @@
                     }
                 },
             vibrationEffect = chipStateSender.transferStatus.vibrationEffect,
+            windowTitle = MediaTttUtils.WINDOW_TITLE_SENDER,
+            wakeReason = MediaTttUtils.WAKE_REASON_SENDER,
+            timeoutMs = chipStateSender.timeout,
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index b39175e..1c0f057 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -135,7 +135,6 @@
 import com.android.systemui.camera.CameraGestureHelper;
 import com.android.systemui.classifier.Classifier;
 import com.android.systemui.classifier.FalsingCollector;
-import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.DisplayId;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.doze.DozeLog;
@@ -231,7 +230,6 @@
 import com.android.systemui.unfold.SysUIUnfoldComponent;
 import com.android.systemui.util.Compile;
 import com.android.systemui.util.LargeScreenUtils;
-import com.android.systemui.util.ListenerSet;
 import com.android.systemui.util.Utils;
 import com.android.systemui.util.time.SystemClock;
 import com.android.wm.shell.animation.FlingAnimationUtils;
@@ -372,7 +370,6 @@
     private final TapAgainViewController mTapAgainViewController;
     private final LargeScreenShadeHeaderController mLargeScreenShadeHeaderController;
     private final RecordingController mRecordingController;
-    private final PanelEventsEmitter mPanelEventsEmitter;
     private final boolean mVibrateOnOpening;
     private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
     private final FlingAnimationUtils mFlingAnimationUtilsClosing;
@@ -880,7 +877,6 @@
             Provider<KeyguardBottomAreaViewController> keyguardBottomAreaViewControllerProvider,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
             NotificationListContainer notificationListContainer,
-            PanelEventsEmitter panelEventsEmitter,
             NotificationStackSizeCalculator notificationStackSizeCalculator,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
             ShadeTransitionController shadeTransitionController,
@@ -993,7 +989,6 @@
         mMediaDataManager = mediaDataManager;
         mTapAgainViewController = tapAgainViewController;
         mSysUiState = sysUiState;
-        mPanelEventsEmitter = panelEventsEmitter;
         pulseExpansionHandler.setPulseExpandAbortListener(() -> {
             if (mQs != null) {
                 mQs.animateHeaderSlidingOut();
@@ -1948,7 +1943,7 @@
     private void setQsExpandImmediate(boolean expandImmediate) {
         if (expandImmediate != mQsExpandImmediate) {
             mQsExpandImmediate = expandImmediate;
-            mPanelEventsEmitter.notifyExpandImmediateChange(expandImmediate);
+            mShadeExpansionStateManager.notifyExpandImmediateChange(expandImmediate);
         }
     }
 
@@ -2674,8 +2669,8 @@
 
         // When expanding QS, let's authenticate the user if possible,
         // this will speed up notification actions.
-        if (height == 0) {
-            mCentralSurfaces.requestFaceAuth(false, FaceAuthApiRequestReason.QS_EXPANDED);
+        if (height == 0 && !mKeyguardStateController.canDismissLockScreen()) {
+            mUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.QS_EXPANDED);
         }
     }
 
@@ -3889,7 +3884,7 @@
         boolean wasRunning = mIsLaunchAnimationRunning;
         mIsLaunchAnimationRunning = running;
         if (wasRunning != mIsLaunchAnimationRunning) {
-            mPanelEventsEmitter.notifyLaunchingActivityChanged(running);
+            mShadeExpansionStateManager.notifyLaunchingActivityChanged(running);
         }
     }
 
@@ -3898,7 +3893,7 @@
         boolean wasClosing = isClosing();
         mClosing = isClosing;
         if (wasClosing != isClosing) {
-            mPanelEventsEmitter.notifyPanelCollapsingChanged(isClosing);
+            mShadeExpansionStateManager.notifyPanelCollapsingChanged(isClosing);
         }
         mAmbientState.setIsClosing(isClosing);
     }
@@ -3933,7 +3928,7 @@
                     mShadeLog.v("onMiddleClicked on Keyguard, mDozingOnDown: false");
                     // Try triggering face auth, this "might" run. Check
                     // KeyguardUpdateMonitor#shouldListenForFace to see when face auth won't run.
-                    boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(true,
+                    boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(
                             FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED);
 
                     if (didFaceAuthRun) {
@@ -4223,8 +4218,8 @@
     /**
      * Sets the dozing state.
      *
-     * @param dozing              {@code true} when dozing.
-     * @param animate             if transition should be animated.
+     * @param dozing  {@code true} when dozing.
+     * @param animate if transition should be animated.
      */
     public void setDozing(boolean dozing, boolean animate) {
         if (dozing == mDozing) return;
@@ -4364,35 +4359,35 @@
     /**
      * Starts fold to AOD animation.
      *
-     * @param startAction invoked when the animation starts.
-     * @param endAction invoked when the animation finishes, also if it was cancelled.
+     * @param startAction  invoked when the animation starts.
+     * @param endAction    invoked when the animation finishes, also if it was cancelled.
      * @param cancelAction invoked when the animation is cancelled, before endAction.
      */
     public void startFoldToAodAnimation(Runnable startAction, Runnable endAction,
             Runnable cancelAction) {
         mView.animate()
-            .translationX(0)
-            .alpha(1f)
-            .setDuration(ANIMATION_DURATION_FOLD_TO_AOD)
-            .setInterpolator(EMPHASIZED_DECELERATE)
-            .setListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationStart(Animator animation) {
-                    startAction.run();
-                }
+                .translationX(0)
+                .alpha(1f)
+                .setDuration(ANIMATION_DURATION_FOLD_TO_AOD)
+                .setInterpolator(EMPHASIZED_DECELERATE)
+                .setListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationStart(Animator animation) {
+                        startAction.run();
+                    }
 
-                @Override
-                public void onAnimationCancel(Animator animation) {
-                    cancelAction.run();
-                }
+                    @Override
+                    public void onAnimationCancel(Animator animation) {
+                        cancelAction.run();
+                    }
 
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    endAction.run();
-                }
-            }).setUpdateListener(anim -> {
-                mKeyguardStatusViewController.animateFoldToAod(anim.getAnimatedFraction());
-            }).start();
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        endAction.run();
+                    }
+                }).setUpdateListener(anim -> {
+                    mKeyguardStatusViewController.animateFoldToAod(anim.getAnimatedFraction());
+                }).start();
     }
 
     /**
@@ -4752,8 +4747,10 @@
     /**
      * Maybe vibrate as panel is opened.
      *
-     * @param openingWithTouch Whether the panel is being opened with touch. If the panel is instead
-     * being opened programmatically (such as by the open panel gesture), we always play haptic.
+     * @param openingWithTouch Whether the panel is being opened with touch. If the panel is
+     *                         instead
+     *                         being opened programmatically (such as by the open panel gesture), we
+     *                         always play haptic.
      */
     private void maybeVibrateOnOpening(boolean openingWithTouch) {
         if (mVibrateOnOpening) {
@@ -4919,10 +4916,12 @@
         animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
         animator.addListener(new AnimatorListenerAdapter() {
             private boolean mCancelled;
+
             @Override
             public void onAnimationCancel(Animator animation) {
                 mCancelled = true;
             }
+
             @Override
             public void onAnimationEnd(Animator animation) {
                 mIsSpringBackAnimation = false;
@@ -4970,7 +4969,7 @@
         if (isNaN(h)) {
             Log.wtf(TAG, "ExpandedHeight set to NaN");
         }
-        mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> {
+        mNotificationShadeWindowController.batchApplyWindowLayoutParams(() -> {
             if (mExpandLatencyTracking && h != 0f) {
                 DejankUtils.postAfterTraversal(
                         () -> mLatencyTracker.onActionEnd(LatencyTracker.ACTION_EXPAND_PANEL));
@@ -5161,7 +5160,7 @@
     /**
      * Create an animator that can also overshoot
      *
-     * @param targetHeight the target height
+     * @param targetHeight    the target height
      * @param overshootAmount the amount of overshoot desired
      */
     private ValueAnimator createHeightAnimator(float targetHeight, float overshootAmount) {
@@ -5917,49 +5916,11 @@
         }
     }
 
-    @SysUISingleton
-    static class PanelEventsEmitter implements NotifPanelEvents {
-
-        private final ListenerSet<Listener> mListeners = new ListenerSet<>();
-
-        @Inject
-        PanelEventsEmitter() {
-        }
-
-        @Override
-        public void registerListener(@androidx.annotation.NonNull @NonNull Listener listener) {
-            mListeners.addIfAbsent(listener);
-        }
-
-        @Override
-        public void unregisterListener(@androidx.annotation.NonNull @NonNull Listener listener) {
-            mListeners.remove(listener);
-        }
-
-        private void notifyLaunchingActivityChanged(boolean isLaunchingActivity) {
-            for (Listener cb : mListeners) {
-                cb.onLaunchingActivityChanged(isLaunchingActivity);
-            }
-        }
-
-        private void notifyPanelCollapsingChanged(boolean isCollapsing) {
-            for (NotifPanelEvents.Listener cb : mListeners) {
-                cb.onPanelCollapsingChanged(isCollapsing);
-            }
-        }
-
-        private void notifyExpandImmediateChange(boolean expandImmediateEnabled) {
-            for (NotifPanelEvents.Listener cb : mListeners) {
-                cb.onExpandImmediateChanged(expandImmediateEnabled);
-            }
-        }
-    }
-
     /** Handles MotionEvents for the Shade. */
     public final class TouchHandler implements View.OnTouchListener {
         private long mLastTouchDownTime = -1L;
 
-        /** @see ViewGroup#onInterceptTouchEvent(MotionEvent)  */
+        /** @see ViewGroup#onInterceptTouchEvent(MotionEvent) */
         public boolean onInterceptTouchEvent(MotionEvent event) {
             if (SPEW_LOGCAT) {
                 Log.v(TAG,
@@ -6158,7 +6119,7 @@
                 mShadeLog.logMotionEvent(event, "onTouch: touch ignored due to instant expanding");
                 return false;
             }
-            if (mTouchDisabled  && event.getActionMasked() != MotionEvent.ACTION_CANCEL) {
+            if (mTouchDisabled && event.getActionMasked() != MotionEvent.ACTION_CANCEL) {
                 mShadeLog.logMotionEvent(event, "onTouch: non-cancel action, touch disabled");
                 return false;
             }
@@ -6366,3 +6327,4 @@
         }
     }
 }
+
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEventsModule.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeEventsModule.java
similarity index 77%
rename from packages/SystemUI/src/com/android/systemui/shade/NotifPanelEventsModule.java
rename to packages/SystemUI/src/com/android/systemui/shade/ShadeEventsModule.java
index 6772384..959c339 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEventsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeEventsModule.java
@@ -21,10 +21,9 @@
 import dagger.Binds;
 import dagger.Module;
 
-/** Provides a {@link NotifPanelEvents} in {@link SysUISingleton} scope. */
+/** Provides a {@link ShadeStateEvents} in {@link SysUISingleton} scope. */
 @Module
-public abstract class NotifPanelEventsModule {
+public abstract class ShadeEventsModule {
     @Binds
-    abstract NotifPanelEvents bindPanelEvents(
-            NotificationPanelViewController.PanelEventsEmitter impl);
+    abstract ShadeStateEvents bindShadeEvents(ShadeExpansionStateManager impl);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
index 7bba74a..667392c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
@@ -20,6 +20,7 @@
 import android.util.Log
 import androidx.annotation.FloatRange
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.ShadeStateEvents.ShadeStateEventsListener
 import com.android.systemui.util.Compile
 import java.util.concurrent.CopyOnWriteArrayList
 import javax.inject.Inject
@@ -30,11 +31,12 @@
  * TODO(b/200063118): Make this class the one source of truth for the state of panel expansion.
  */
 @SysUISingleton
-class ShadeExpansionStateManager @Inject constructor() {
+class ShadeExpansionStateManager @Inject constructor() : ShadeStateEvents {
 
     private val expansionListeners = CopyOnWriteArrayList<ShadeExpansionListener>()
     private val qsExpansionListeners = CopyOnWriteArrayList<ShadeQsExpansionListener>()
     private val stateListeners = CopyOnWriteArrayList<ShadeStateListener>()
+    private val shadeStateEventsListeners = CopyOnWriteArrayList<ShadeStateEventsListener>()
 
     @PanelState private var state: Int = STATE_CLOSED
     @FloatRange(from = 0.0, to = 1.0) private var fraction: Float = 0f
@@ -79,6 +81,14 @@
         stateListeners.remove(listener)
     }
 
+    override fun addShadeStateEventsListener(listener: ShadeStateEventsListener) {
+        shadeStateEventsListeners.addIfAbsent(listener)
+    }
+
+    override fun removeShadeStateEventsListener(listener: ShadeStateEventsListener) {
+        shadeStateEventsListeners.remove(listener)
+    }
+
     /** Returns true if the panel is currently closed and false otherwise. */
     fun isClosed(): Boolean = state == STATE_CLOSED
 
@@ -162,6 +172,24 @@
         stateListeners.forEach { it.onPanelStateChanged(state) }
     }
 
+    fun notifyLaunchingActivityChanged(isLaunchingActivity: Boolean) {
+        for (cb in shadeStateEventsListeners) {
+            cb.onLaunchingActivityChanged(isLaunchingActivity)
+        }
+    }
+
+    fun notifyPanelCollapsingChanged(isCollapsing: Boolean) {
+        for (cb in shadeStateEventsListeners) {
+            cb.onPanelCollapsingChanged(isCollapsing)
+        }
+    }
+
+    fun notifyExpandImmediateChange(expandImmediateEnabled: Boolean) {
+        for (cb in shadeStateEventsListeners) {
+            cb.onExpandImmediateChanged(expandImmediateEnabled)
+        }
+    }
+
     private fun debugLog(msg: String) {
         if (!DEBUG) return
         Log.v(TAG, msg)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEvents.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateEvents.kt
similarity index 70%
rename from packages/SystemUI/src/com/android/systemui/shade/NotifPanelEvents.kt
rename to packages/SystemUI/src/com/android/systemui/shade/ShadeStateEvents.kt
index 4558061..56bb1a6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEvents.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateEvents.kt
@@ -16,27 +16,25 @@
 
 package com.android.systemui.shade
 
-/** Provides certain notification panel events.  */
-interface NotifPanelEvents {
+/** Provides certain notification panel events. */
+interface ShadeStateEvents {
 
-    /** Registers callbacks to be invoked when notification panel events occur.  */
-    fun registerListener(listener: Listener)
+    /** Registers callbacks to be invoked when notification panel events occur. */
+    fun addShadeStateEventsListener(listener: ShadeStateEventsListener)
 
-    /** Unregisters callbacks previously registered via [registerListener]  */
-    fun unregisterListener(listener: Listener)
+    /** Unregisters callbacks previously registered via [addShadeStateEventsListener] */
+    fun removeShadeStateEventsListener(listener: ShadeStateEventsListener)
 
     /** Callbacks for certain notification panel events. */
-    interface Listener {
+    interface ShadeStateEventsListener {
 
         /** Invoked when the notification panel starts or stops collapsing. */
-        @JvmDefault
-        fun onPanelCollapsingChanged(isCollapsing: Boolean) {}
+        @JvmDefault fun onPanelCollapsingChanged(isCollapsing: Boolean) {}
 
         /**
          * Invoked when the notification panel starts or stops launching an [android.app.Activity].
          */
-        @JvmDefault
-        fun onLaunchingActivityChanged(isLaunchingActivity: Boolean) {}
+        @JvmDefault fun onLaunchingActivityChanged(isLaunchingActivity: Boolean) {}
 
         /**
          * Invoked when the "expand immediate" attribute changes.
@@ -47,7 +45,6 @@
          * Another example is when full QS is showing, and we swipe up from the bottom. Instead of
          * going to QQS, the panel fully collapses.
          */
-        @JvmDefault
-        fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) {}
+        @JvmDefault fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) {}
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
index d3bc257..3002a68 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
@@ -28,7 +28,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.shade.NotifPanelEvents;
+import com.android.systemui.shade.ShadeStateEvents;
 import com.android.systemui.statusbar.notification.collection.GroupEntry;
 import com.android.systemui.statusbar.notification.collection.ListEntry;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -55,12 +55,12 @@
 // TODO(b/204468557): Move to @CoordinatorScope
 @SysUISingleton
 public class VisualStabilityCoordinator implements Coordinator, Dumpable,
-        NotifPanelEvents.Listener {
+        ShadeStateEvents.ShadeStateEventsListener {
     public static final String TAG = "VisualStability";
     public static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE);
     private final DelayableExecutor mDelayableExecutor;
     private final HeadsUpManager mHeadsUpManager;
-    private final NotifPanelEvents mNotifPanelEvents;
+    private final ShadeStateEvents mShadeStateEvents;
     private final StatusBarStateController mStatusBarStateController;
     private final VisualStabilityProvider mVisualStabilityProvider;
     private final WakefulnessLifecycle mWakefulnessLifecycle;
@@ -92,7 +92,7 @@
             DelayableExecutor delayableExecutor,
             DumpManager dumpManager,
             HeadsUpManager headsUpManager,
-            NotifPanelEvents notifPanelEvents,
+            ShadeStateEvents shadeStateEvents,
             StatusBarStateController statusBarStateController,
             VisualStabilityProvider visualStabilityProvider,
             WakefulnessLifecycle wakefulnessLifecycle) {
@@ -101,7 +101,7 @@
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mStatusBarStateController = statusBarStateController;
         mDelayableExecutor = delayableExecutor;
-        mNotifPanelEvents = notifPanelEvents;
+        mShadeStateEvents = shadeStateEvents;
 
         dumpManager.registerDumpable(this);
     }
@@ -114,7 +114,7 @@
 
         mStatusBarStateController.addCallback(mStatusBarStateControllerListener);
         mPulsing = mStatusBarStateController.isPulsing();
-        mNotifPanelEvents.registerListener(this);
+        mShadeStateEvents.addShadeStateEventsListener(this);
 
         pipeline.setVisualStabilityManager(mNotifStabilityManager);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index da4cced..ff63891 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -32,8 +32,8 @@
 import com.android.systemui.people.widget.PeopleSpaceWidgetManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.settings.UserContextProvider;
-import com.android.systemui.shade.NotifPanelEventsModule;
 import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeEventsModule;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.notification.AssistantFeedbackController;
 import com.android.systemui.statusbar.notification.collection.NotifInflaterImpl;
@@ -93,7 +93,7 @@
 @Module(includes = {
         CoordinatorsModule.class,
         KeyguardNotificationVisibilityProviderModule.class,
-        NotifPanelEventsModule.class,
+        ShadeEventsModule.class,
         NotifPipelineChoreographerModule.class,
         NotificationSectionHeadersModule.class,
 })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index 70cf56d..2504fc1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -41,7 +41,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.statusbar.RegisterStatusBarResult;
 import com.android.keyguard.AuthKeyguardMessageArea;
-import com.android.keyguard.FaceAuthApiRequestReason;
 import com.android.systemui.Dumpable;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.RemoteTransitionAdapter;
@@ -230,13 +229,6 @@
 
     boolean isShadeDisabled();
 
-    /**
-     * Request face auth to initiated
-     * @param userInitiatedRequest Whether this was a user initiated request
-     * @param reason Reason why face auth was triggered.
-     */
-    void requestFaceAuth(boolean userInitiatedRequest, @FaceAuthApiRequestReason String reason);
-
     @Override
     void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade,
             int flags);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 43bc99f..d227ed3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -122,7 +122,6 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.statusbar.RegisterStatusBarResult;
 import com.android.keyguard.AuthKeyguardMessageArea;
-import com.android.keyguard.FaceAuthApiRequestReason;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.keyguard.ViewMediatorCallback;
@@ -1614,18 +1613,6 @@
         return (mDisabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0;
     }
 
-    /**
-     * Asks {@link KeyguardUpdateMonitor} to run face auth.
-     */
-    @Override
-    public void requestFaceAuth(boolean userInitiatedRequest,
-            @FaceAuthApiRequestReason String reason) {
-        if (!mKeyguardStateController.canDismissLockScreen()) {
-            mKeyguardUpdateMonitor.requestFaceAuth(
-                    userInitiatedRequest, reason);
-        }
-    }
-
     private void updateReportRejectedTouchVisibility() {
         if (mReportRejectedTouch == null) {
             return;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
index 5e26cf0..4550cb2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
@@ -73,7 +73,6 @@
             isListening = false
             updateListeningState()
             keyguardUpdateMonitor.requestFaceAuth(
-                true,
                 FaceAuthApiRequestReason.PICK_UP_GESTURE_TRIGGERED
             )
             keyguardUpdateMonitor.requestActiveUnlock(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 9767103..c189ace 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -649,37 +649,6 @@
         return mNumDots > 0;
     }
 
-    /**
-     * If the overflow is in the range [1, max_dots - 1) (basically 1 or 2 dots), then
-     * extra padding will have to be accounted for
-     *
-     * This method has no meaning for non-static containers
-     */
-    public boolean hasPartialOverflow() {
-        return mNumDots > 0 && mNumDots < MAX_DOTS;
-    }
-
-    /**
-     * Get padding that can account for extra dots up to the max. The only valid values for
-     * this method are for 1 or 2 dots.
-     * @return only extraDotPadding or extraDotPadding * 2
-     */
-    public int getPartialOverflowExtraPadding() {
-        if (!hasPartialOverflow()) {
-            return 0;
-        }
-
-        int partialOverflowAmount = (MAX_DOTS - mNumDots) * (mStaticDotDiameter + mDotPadding);
-
-        int adjustedWidth = getFinalTranslationX() + partialOverflowAmount;
-        // In case we actually give too much padding...
-        if (adjustedWidth > getWidth()) {
-            partialOverflowAmount = getWidth() - getFinalTranslationX();
-        }
-
-        return partialOverflowAmount;
-    }
-
     // Give some extra room for btw notifications if we can
     public int getNoOverflowExtraPadding() {
         if (mNumDots != 0) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt
new file mode 100644
index 0000000..da87f73
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.pipeline.mobile.data.model
+
+import android.telephony.TelephonyManager.DATA_CONNECTED
+import android.telephony.TelephonyManager.DATA_CONNECTING
+import android.telephony.TelephonyManager.DATA_DISCONNECTED
+import android.telephony.TelephonyManager.DATA_DISCONNECTING
+import android.telephony.TelephonyManager.DataState
+
+/** Internal enum representation of the telephony data connection states */
+enum class DataConnectionState(@DataState val dataState: Int) {
+    Connected(DATA_CONNECTED),
+    Connecting(DATA_CONNECTING),
+    Disconnected(DATA_DISCONNECTED),
+    Disconnecting(DATA_DISCONNECTING),
+}
+
+fun @receiver:DataState Int.toDataConnectionType(): DataConnectionState =
+    when (this) {
+        DATA_CONNECTED -> DataConnectionState.Connected
+        DATA_CONNECTING -> DataConnectionState.Connecting
+        DATA_DISCONNECTED -> DataConnectionState.Disconnected
+        DATA_DISCONNECTING -> DataConnectionState.Disconnecting
+        else -> throw IllegalArgumentException("unknown data state received")
+    }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
index eaba0e9..6341a11 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
@@ -28,6 +28,7 @@
 import android.telephony.TelephonyDisplayInfo
 import android.telephony.TelephonyManager
 import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Disconnected
 
 /**
  * Data class containing all of the relevant information for a particular line of service, known as
@@ -49,14 +50,14 @@
     @IntRange(from = 0, to = 4)
     val primaryLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN,
 
-    /** Comes directly from [DataConnectionStateListener.onDataConnectionStateChanged] */
-    val dataConnectionState: Int? = null,
+    /** Mapped from [DataConnectionStateListener.onDataConnectionStateChanged] */
+    val dataConnectionState: DataConnectionState = Disconnected,
 
     /** From [DataActivityListener.onDataActivity]. See [TelephonyManager] for the values */
     @DataActivityType val dataActivityDirection: Int? = null,
 
     /** From [CarrierNetworkListener.onCarrierNetworkChange] */
-    val carrierNetworkChangeActive: Boolean? = null,
+    val carrierNetworkChangeActive: Boolean = false,
 
     /**
      * From [DisplayInfoListener.onDisplayInfoChanged].
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index 45284cf..06e8f46 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -31,7 +31,9 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
 import java.lang.IllegalStateException
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
@@ -42,7 +44,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
 /**
@@ -62,13 +64,15 @@
      * listener + model.
      */
     val subscriptionModelFlow: Flow<MobileSubscriptionModel>
+    /** Observable tracking [TelephonyManager.isDataConnectionAllowed] */
+    val dataEnabled: Flow<Boolean>
 }
 
 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
 @OptIn(ExperimentalCoroutinesApi::class)
 class MobileConnectionRepositoryImpl(
     private val subId: Int,
-    telephonyManager: TelephonyManager,
+    private val telephonyManager: TelephonyManager,
     bgDispatcher: CoroutineDispatcher,
     logger: ConnectivityPipelineLogger,
     scope: CoroutineScope,
@@ -127,7 +131,8 @@
                             dataState: Int,
                             networkType: Int
                         ) {
-                            state = state.copy(dataConnectionState = dataState)
+                            state =
+                                state.copy(dataConnectionState = dataState.toDataConnectionType())
                             trySend(state)
                         }
 
@@ -160,10 +165,21 @@
                 telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
                 awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
             }
-            .onEach { logger.logOutputChange("mobileSubscriptionModel", it.toString()) }
+            .logOutputChange(logger, "MobileSubscriptionModel")
             .stateIn(scope, SharingStarted.WhileSubscribed(), state)
     }
 
+    /**
+     * There are a few cases where we will need to poll [TelephonyManager] so we can update some
+     * internal state where callbacks aren't provided. Any of those events should be merged into
+     * this flow, which can be used to trigger the polling.
+     */
+    private val telephonyPollingEvent: Flow<Unit> = subscriptionModelFlow.map {}
+
+    override val dataEnabled: Flow<Boolean> = telephonyPollingEvent.map { dataConnectionAllowed() }
+
+    private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed
+
     class Factory
     @Inject
     constructor(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index 15f4acc..f99d278c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -29,6 +29,9 @@
 import kotlinx.coroutines.flow.map
 
 interface MobileIconInteractor {
+    /** Observable for the data enabled state of this connection */
+    val isDataEnabled: Flow<Boolean>
+
     /** Observable for RAT type (network type) indicator */
     val networkTypeIconGroup: Flow<MobileIconGroup>
 
@@ -54,6 +57,8 @@
 ) : MobileIconInteractor {
     private val mobileStatusInfo = connectionRepository.subscriptionModelFlow
 
+    override val isDataEnabled: Flow<Boolean> = connectionRepository.dataEnabled
+
     /** Observable for the current RAT indicator icon ([MobileIconGroup]) */
     override val networkTypeIconGroup: Flow<MobileIconGroup> =
         combine(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index cd411a4..614d583 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -23,12 +23,14 @@
 import com.android.settingslib.mobile.TelephonyIcons
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
 import com.android.systemui.util.CarrierConfigTracker
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -47,28 +49,38 @@
  * icon
  */
 interface MobileIconsInteractor {
+    /** List of subscriptions, potentially filtered for CBRS */
     val filteredSubscriptions: Flow<List<SubscriptionInfo>>
+    /** The icon mapping from network type to [MobileIconGroup] for the default subscription */
     val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>
+    /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
     val defaultMobileIconGroup: Flow<MobileIconGroup>
+    /** True once the user has been set up */
     val isUserSetup: Flow<Boolean>
+    /**
+     * Vends out a [MobileIconInteractor] tracking the [MobileConnectionRepository] for the given
+     * subId. Will throw if the ID is invalid
+     */
     fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor
 }
 
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class MobileIconsInteractorImpl
 @Inject
 constructor(
-    private val mobileSubscriptionRepo: MobileConnectionsRepository,
+    private val mobileConnectionsRepo: MobileConnectionsRepository,
     private val carrierConfigTracker: CarrierConfigTracker,
     private val mobileMappingsProxy: MobileMappingsProxy,
     userSetupRepo: UserSetupRepository,
     @Application private val scope: CoroutineScope,
 ) : MobileIconsInteractor {
     private val activeMobileDataSubscriptionId =
-        mobileSubscriptionRepo.activeMobileDataSubscriptionId
+        mobileConnectionsRepo.activeMobileDataSubscriptionId
 
     private val unfilteredSubscriptions: Flow<List<SubscriptionInfo>> =
-        mobileSubscriptionRepo.subscriptionsFlow
+        mobileConnectionsRepo.subscriptionsFlow
 
     /**
      * Generally, SystemUI wants to show iconography for each subscription that is listed by
@@ -119,13 +131,13 @@
      * subscription Id. This mapping is the same for every subscription.
      */
     override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> =
-        mobileSubscriptionRepo.defaultDataSubRatConfig
+        mobileConnectionsRepo.defaultDataSubRatConfig
             .map { mobileMappingsProxy.mapIconSets(it) }
             .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = mapOf())
 
     /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */
     override val defaultMobileIconGroup: StateFlow<MobileIconGroup> =
-        mobileSubscriptionRepo.defaultDataSubRatConfig
+        mobileConnectionsRepo.defaultDataSubRatConfig
             .map { mobileMappingsProxy.getDefaultIcons(it) }
             .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = TelephonyIcons.G)
 
@@ -137,6 +149,6 @@
             defaultMobileIconMapping,
             defaultMobileIconGroup,
             mobileMappingsProxy,
-            mobileSubscriptionRepo.getRepoForSubId(subId),
+            mobileConnectionsRepo.getRepoForSubId(subId),
         )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
index cc8f6dd..8131739 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -28,7 +28,6 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
 
 /**
  * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
@@ -59,12 +58,18 @@
 
     /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
     var networkTypeIcon: Flow<Icon?> =
-        iconInteractor.networkTypeIconGroup.map {
-            val desc =
-                if (it.dataContentDescription != 0)
-                    ContentDescription.Resource(it.dataContentDescription)
-                else null
-            Icon.Resource(it.dataType, desc)
+        combine(iconInteractor.networkTypeIconGroup, iconInteractor.isDataEnabled) {
+            networkTypeIconGroup,
+            isDataEnabled ->
+            if (!isDataEnabled) {
+                null
+            } else {
+                val desc =
+                    if (networkTypeIconGroup.dataContentDescription != 0)
+                        ContentDescription.Resource(networkTypeIconGroup.dataContentDescription)
+                    else null
+                Icon.Resource(networkTypeIconGroup.dataType, desc)
+            }
         }
 
     var tint: Flow<Int> = flowOf(Color.CYAN)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt
index 118b94c7..6efb10f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt
@@ -34,7 +34,7 @@
 @Inject
 constructor(dumpManager: DumpManager, telephonyManager: TelephonyManager) : Dumpable {
     init {
-        dumpManager.registerDumpable("$SB_LOGGING_TAG:ConnectivityConstants", this)
+        dumpManager.registerDumpable("${SB_LOGGING_TAG}Constants", this)
     }
 
     /** True if this device has the capability for data connections and false otherwise. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt
index 6b1750d..45c6d46 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt
@@ -62,7 +62,7 @@
     tunerService: TunerService,
 ) : ConnectivityRepository, Dumpable {
     init {
-        dumpManager.registerDumpable("$SB_LOGGING_TAG:ConnectivityRepository", this)
+        dumpManager.registerDumpable("${SB_LOGGING_TAG}Repository", this)
     }
 
     // The default set of hidden icons to use if we don't get any from [TunerService].
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
index 0eb4b0d..3c0eb91 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
@@ -35,7 +35,7 @@
         dumpManager: DumpManager,
 ) : Dumpable {
     init {
-        dumpManager.registerDumpable("$SB_LOGGING_TAG:WifiConstants", this)
+        dumpManager.registerDumpable("${SB_LOGGING_TAG}WifiConstants", this)
     }
 
     /** True if we should show the activityIn/activityOut icons and false otherwise. */
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index f0a50de..637fac0 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -44,11 +44,6 @@
  *
  * The generic type T is expected to contain all the information necessary for the subclasses to
  * display the view in a certain state, since they receive <T> in [updateView].
- *
- * @property windowTitle the title to use for the window that displays the temporary view. Should be
- *   normally cased, like "Window Title".
- * @property wakeReason a string used for logging if we needed to wake the screen in order to
- *   display the temporary view. Should be screaming snake cased, like WAKE_REASON.
  */
 abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : TemporaryViewLogger>(
     internal val context: Context,
@@ -59,8 +54,6 @@
     private val configurationController: ConfigurationController,
     private val powerManager: PowerManager,
     @LayoutRes private val viewLayoutRes: Int,
-    private val windowTitle: String,
-    private val wakeReason: String,
 ) : CoreStartable {
     /**
      * Window layout params that will be used as a starting point for the [windowLayoutParams] of
@@ -72,7 +65,6 @@
         type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR
         flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
             WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
-        title = windowTitle
         format = PixelFormat.TRANSLUCENT
         setTrustedOverlay()
     }
@@ -100,29 +92,40 @@
     fun displayView(newInfo: T) {
         val currentDisplayInfo = displayInfo
 
-        if (currentDisplayInfo != null) {
+        if (currentDisplayInfo != null &&
+            currentDisplayInfo.info.windowTitle == newInfo.windowTitle) {
+            // We're already displaying information in the correctly-titled window, so we just need
+            // to update the view.
             currentDisplayInfo.info = newInfo
             updateView(currentDisplayInfo.info, currentDisplayInfo.view)
         } else {
-            // The view is new, so set up all our callbacks and inflate the view
+            if (currentDisplayInfo != null) {
+                // We're already displaying information but that information is under a different
+                // window title. So, we need to remove the old window with the old title and add a
+                // new window with the new title.
+                removeView(removalReason = "New info has new window title: ${newInfo.windowTitle}")
+            }
+
+            // At this point, we're guaranteed to no longer be displaying a view.
+            // So, set up all our callbacks and inflate the view.
             configurationController.addCallback(displayScaleListener)
             // Wake the screen if necessary so the user will see the view. (Per b/239426653, we want
             // the view to show over the dream state, so we should only wake up if the screen is
             // completely off.)
             if (!powerManager.isScreenOn) {
                 powerManager.wakeUp(
-                        SystemClock.uptimeMillis(),
-                        PowerManager.WAKE_REASON_APPLICATION,
-                        "com.android.systemui:$wakeReason",
+                    SystemClock.uptimeMillis(),
+                    PowerManager.WAKE_REASON_APPLICATION,
+                    "com.android.systemui:${newInfo.wakeReason}",
                 )
             }
-            logger.logChipAddition()
+            logger.logViewAddition(newInfo.windowTitle)
             inflateAndUpdateView(newInfo)
         }
 
         // Cancel and re-set the view timeout each time we get a new state.
         val timeout = accessibilityManager.getRecommendedTimeoutMillis(
-            newInfo.getTimeoutMs().toInt(),
+            newInfo.timeoutMs,
             // Not all views have controls so FLAG_CONTENT_CONTROLS might be superfluous, but
             // include it just to be safe.
             FLAG_CONTENT_ICONS or FLAG_CONTENT_TEXT or FLAG_CONTENT_CONTROLS
@@ -147,7 +150,12 @@
         val newDisplayInfo = DisplayInfo(newView, newInfo)
         displayInfo = newDisplayInfo
         updateView(newDisplayInfo.info, newDisplayInfo.view)
-        windowManager.addView(newView, windowLayoutParams)
+
+        val paramsWithTitle = WindowManager.LayoutParams().also {
+            it.copyFrom(windowLayoutParams)
+            it.title = newInfo.windowTitle
+        }
+        windowManager.addView(newView, paramsWithTitle)
         animateViewIn(newView)
     }
 
@@ -177,7 +185,7 @@
         val currentView = currentDisplayInfo.view
         animateViewOut(currentView) { windowManager.removeView(currentView) }
 
-        logger.logChipRemoval(removalReason)
+        logger.logViewRemoval(removalReason)
         configurationController.removeCallback(displayScaleListener)
         // Re-set to null immediately (instead as part of the animation end runnable) so
         // that if a new view event comes in while this view is animating out, we still display the
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt
index 4fe753a..cbb5002 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt
@@ -19,12 +19,24 @@
 /**
  * A superclass view state used with [TemporaryViewDisplayController].
  */
-interface TemporaryViewInfo {
+abstract class TemporaryViewInfo {
     /**
-     * Returns the amount of time the given view state should display on the screen before it times
-     * out and disappears.
+     * The title to use for the window that displays the temporary view. Should be normally cased,
+     * like "Window Title".
      */
-    fun getTimeoutMs(): Long = DEFAULT_TIMEOUT_MILLIS
+    abstract val windowTitle: String
+
+    /**
+     * A string used for logging if we needed to wake the screen in order to display the temporary
+     * view. Should be screaming snake cased, like WAKE_REASON.
+     */
+    abstract val wakeReason: String
+
+    /**
+     * The amount of time the given view state should display on the screen before it times out and
+     * disappears.
+     */
+    open val timeoutMs: Int = DEFAULT_TIMEOUT_MILLIS
 }
 
-const val DEFAULT_TIMEOUT_MILLIS = 10000L
+const val DEFAULT_TIMEOUT_MILLIS = 10000
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
index a7185cb..428a104 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt
@@ -24,13 +24,13 @@
     internal val buffer: LogBuffer,
     internal val tag: String,
 ) {
-    /** Logs that we added the chip to a new window. */
-    fun logChipAddition() {
-        buffer.log(tag, LogLevel.DEBUG, {}, { "Chip added" })
+    /** Logs that we added the view in a window titled [windowTitle]. */
+    fun logViewAddition(windowTitle: String) {
+        buffer.log(tag, LogLevel.DEBUG, { str1 = windowTitle }, { "View added. window=$str1" })
     }
 
     /** Logs that we removed the chip for the given [reason]. */
-    fun logChipRemoval(reason: String) {
-        buffer.log(tag, LogLevel.DEBUG, { str1 = reason }, { "Chip removed due to $str1" })
+    fun logViewRemoval(reason: String) {
+        buffer.log(tag, LogLevel.DEBUG, { str1 = reason }, { "View removed due to: $str1" })
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index b8930a4..87b6e8d 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -38,9 +38,6 @@
 import com.android.systemui.common.ui.binder.TextViewBinder
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger
-import com.android.systemui.media.taptotransfer.common.MediaTttUtils
-import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -64,14 +61,11 @@
  * Only one chipbar may be shown at a time.
  * TODO(b/245610654): Should we just display whichever chipbar was most recently requested, or do we
  *   need to maintain a priority ordering?
- *
- * TODO(b/245610654): Remove all media-related items from this class so it's just for generic
- *   chipbars.
  */
 @SysUISingleton
 open class ChipbarCoordinator @Inject constructor(
         context: Context,
-        @MediaTttSenderLogger logger: MediaTttLogger,
+        logger: ChipbarLogger,
         windowManager: WindowManager,
         @Main mainExecutor: DelayableExecutor,
         accessibilityManager: AccessibilityManager,
@@ -81,7 +75,7 @@
         private val falsingCollector: FalsingCollector,
         private val viewUtil: ViewUtil,
         private val vibratorHelper: VibratorHelper,
-) : TemporaryViewDisplayController<ChipbarInfo, MediaTttLogger>(
+) : TemporaryViewDisplayController<ChipbarInfo, ChipbarLogger>(
         context,
         logger,
         windowManager,
@@ -90,8 +84,6 @@
         configurationController,
         powerManager,
         R.layout.chipbar,
-        MediaTttUtils.WINDOW_TITLE,
-        MediaTttUtils.WAKE_REASON,
 ) {
 
     private lateinit var parent: ChipbarRootView
@@ -106,7 +98,16 @@
         newInfo: ChipbarInfo,
         currentView: ViewGroup
     ) {
-        // TODO(b/245610654): Adding logging here.
+        logger.logViewUpdate(
+            newInfo.windowTitle,
+            newInfo.text.loadText(context),
+            when (newInfo.endItem) {
+                null -> "null"
+                is ChipbarEndItem.Loading -> "loading"
+                is ChipbarEndItem.Error -> "error"
+                is ChipbarEndItem.Button -> "button(${newInfo.endItem.text.loadText(context)})"
+            }
+        )
 
         // Detect falsing touches on the chip.
         parent = currentView.requireViewById(R.id.chipbar_root_view)
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
index 57fde87..6237365 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
@@ -37,7 +37,10 @@
     val text: Text,
     val endItem: ChipbarEndItem?,
     val vibrationEffect: VibrationEffect? = null,
-) : TemporaryViewInfo
+    override val windowTitle: String,
+    override val wakeReason: String,
+    override val timeoutMs: Int,
+) : TemporaryViewInfo()
 
 /** The possible items to display at the end of the chipbar. */
 sealed class ChipbarEndItem {
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt
new file mode 100644
index 0000000..e477cd6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.temporarydisplay.chipbar
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.temporarydisplay.TemporaryViewLogger
+import com.android.systemui.temporarydisplay.dagger.ChipbarLog
+import javax.inject.Inject
+
+/** A logger for the chipbar. */
+@SysUISingleton
+class ChipbarLogger
+@Inject
+constructor(
+    @ChipbarLog buffer: LogBuffer,
+) : TemporaryViewLogger(buffer, "ChipbarLog") {
+    /**
+     * Logs that the chipbar was updated to display in a window named [windowTitle], with [text] and
+     * [endItemDesc].
+     */
+    fun logViewUpdate(windowTitle: String, text: String?, endItemDesc: String) {
+        buffer.log(
+            tag,
+            LogLevel.DEBUG,
+            {
+                str1 = windowTitle
+                str2 = text
+                str3 = endItemDesc
+            },
+            { "Chipbar updated. window=$str1 text=$str2 endItem=$str3" }
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/ChipbarLog.kt
similarity index 70%
copy from packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt
copy to packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/ChipbarLog.kt
index 581dafa3..5f101f2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/ChipbarLog.kt
@@ -14,10 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.systemui.keyguard.domain.model
+package com.android.systemui.temporarydisplay.dagger
 
-/** Enumerates all possible positions for quick affordances that can appear on the lock-screen. */
-enum class KeyguardQuickAffordancePosition {
-    BOTTOM_START,
-    BOTTOM_END,
-}
+import javax.inject.Qualifier
+
+/** Status bar connectivity logs in table format. */
+@Qualifier
+@MustBeDocumented
+@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
+annotation class ChipbarLog
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt
new file mode 100644
index 0000000..cf0a183
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.temporarydisplay.dagger
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBufferFactory
+import com.android.systemui.plugins.log.LogBuffer
+import dagger.Module
+import dagger.Provides
+
+@Module
+interface TemporaryDisplayModule {
+    @Module
+    companion object {
+        @JvmStatic
+        @Provides
+        @SysUISingleton
+        @ChipbarLog
+        fun provideChipbarLogBuffer(factory: LogBufferFactory): LogBuffer {
+            return factory.create("ChipbarLog", 40)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index b885d54..f9bec65 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -486,7 +486,7 @@
 
         registeredSwipeListener.onSwipeUp();
 
-        verify(mKeyguardUpdateMonitor).requestFaceAuth(true,
+        verify(mKeyguardUpdateMonitor).requestFaceAuth(
                 FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER);
     }
 
@@ -499,16 +499,15 @@
         registeredSwipeListener.onSwipeUp();
 
         verify(mKeyguardUpdateMonitor, never())
-                .requestFaceAuth(true,
-                        FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER);
+                .requestFaceAuth(FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER);
     }
 
     @Test
     public void onSwipeUp_whenFaceDetectionIsTriggered_hidesBouncerMessage() {
         KeyguardSecurityContainer.SwipeListener registeredSwipeListener =
                 getRegisteredSwipeListener();
-        when(mKeyguardUpdateMonitor.requestFaceAuth(true,
-                FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)).thenReturn(true);
+        when(mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER))
+                .thenReturn(true);
         setupGetSecurityView();
 
         registeredSwipeListener.onSwipeUp();
@@ -520,8 +519,8 @@
     public void onSwipeUp_whenFaceDetectionIsNotTriggered_retainsBouncerMessage() {
         KeyguardSecurityContainer.SwipeListener registeredSwipeListener =
                 getRegisteredSwipeListener();
-        when(mKeyguardUpdateMonitor.requestFaceAuth(true,
-                FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)).thenReturn(false);
+        when(mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER))
+                .thenReturn(false);
         setupGetSecurityView();
 
         registeredSwipeListener.onSwipeUp();
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 66be6ec..680c3b8 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -676,7 +676,7 @@
         bouncerFullyVisibleAndNotGoingToSleep();
         mTestableLooper.processAllMessages();
 
-        boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(true,
+        boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(
                 NOTIFICATION_PANEL_CLICKED);
 
         assertThat(didFaceAuthRun).isTrue();
@@ -688,7 +688,7 @@
         biometricsDisabledForCurrentUser();
         mTestableLooper.processAllMessages();
 
-        boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(true,
+        boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(
                 NOTIFICATION_PANEL_CLICKED);
 
         assertThat(didFaceAuthRun).isFalse();
@@ -707,8 +707,7 @@
         // Stop scanning when bouncer becomes visible
         setKeyguardBouncerVisibility(true);
         clearInvocations(mFaceManager);
-        mKeyguardUpdateMonitor.requestFaceAuth(true,
-                FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
+        mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
         verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
                 anyBoolean());
     }
@@ -1695,7 +1694,7 @@
     }
 
     private void triggerSuccessfulFaceAuth() {
-        mKeyguardUpdateMonitor.requestFaceAuth(true, FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
+        mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
         verify(mFaceManager).authenticate(any(),
                 any(),
                 mAuthenticationCallbackCaptor.capture(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
index bf6d574..78ee627 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
@@ -43,6 +43,7 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
@@ -54,6 +55,9 @@
     @Rule
     public MockitoRule mockito = MockitoJUnit.rule();
 
+    @Mock
+    private DismissAnimationController.DismissCallback mStubDismissCallback;
+
     private RecyclerView mStubListView;
     private MenuView mMenuView;
     private MenuItemAccessibilityDelegate mMenuItemAccessibilityDelegate;
@@ -87,7 +91,7 @@
 
         mMenuItemAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mStubListView, info);
 
-        assertThat(info.getActionList().size()).isEqualTo(5);
+        assertThat(info.getActionList().size()).isEqualTo(6);
     }
 
     @Test
@@ -156,6 +160,17 @@
     }
 
     @Test
+    public void performRemoveMenuAction_success() {
+        mMenuAnimationController.setDismissCallback(mStubDismissCallback);
+        final boolean removeMenuAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_remove_menu, null);
+
+        assertThat(removeMenuAction).isTrue();
+        verify(mMenuAnimationController).removeMenu();
+    }
+
+    @Test
     public void performFocusAction_fadeIn() {
         mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
                 ACTION_ACCESSIBILITY_FOCUS, null);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index d1107c6..45b8ce1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -41,10 +41,15 @@
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakePromptRepository
+import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
 import org.junit.After
 import org.junit.Rule
 import org.junit.Test
@@ -80,6 +85,15 @@
     @Mock
     lateinit var interactionJankMonitor: InteractionJankMonitor
 
+    private val biometricPromptRepository = FakePromptRepository()
+    private val credentialInteractor = FakeCredentialInteractor()
+    private val bpCredentialInteractor = BiometricPromptCredentialInteractor(
+        Dispatchers.Main.immediate,
+        biometricPromptRepository,
+        credentialInteractor
+    )
+    private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor)
+
     private var authContainer: TestAuthContainerView? = null
 
     @After
@@ -466,6 +480,8 @@
         userManager,
         lockPatternUtils,
         interactionJankMonitor,
+        { bpCredentialInteractor },
+        { credentialViewModel },
         Handler(TestableLooper.get(this).looper),
         FakeExecutor(FakeSystemClock())
     ) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
index 88a806d..4dd46ed 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -89,6 +89,8 @@
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
@@ -165,6 +167,11 @@
     private UdfpsLogger mUdfpsLogger;
     @Mock
     private InteractionJankMonitor mInteractionJankMonitor;
+    @Mock
+    private BiometricPromptCredentialInteractor mBiometricPromptCredentialInteractor;
+    @Mock
+    private CredentialViewModel mCredentialViewModel;
+
     @Captor
     private ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mFpAuthenticatorsRegisteredCaptor;
     @Captor
@@ -1009,6 +1016,7 @@
                     fingerprintManager, faceManager, udfpsControllerFactory,
                     sidefpsControllerFactory, mDisplayManager, mWakefulnessLifecycle,
                     mUserManager, mLockPatternUtils, mUdfpsLogger, statusBarStateController,
+                    () -> mBiometricPromptCredentialInteractor, () -> mCredentialViewModel,
                     mInteractionJankMonitor, mHandler, mBackgroundExecutor, vibratorHelper);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
index 8820c16..1379a0e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
@@ -22,12 +22,11 @@
 import android.hardware.biometrics.ComponentInfoInternal
 import android.hardware.biometrics.PromptInfo
 import android.hardware.biometrics.SensorProperties
-import android.hardware.face.FaceSensorPropertiesInternal
 import android.hardware.face.FaceSensorProperties
+import android.hardware.face.FaceSensorPropertiesInternal
 import android.hardware.fingerprint.FingerprintSensorProperties
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
 import android.os.Bundle
-
 import android.testing.ViewUtils
 import android.view.LayoutInflater
 
@@ -83,26 +82,31 @@
 internal fun fingerprintSensorPropertiesInternal(
     ids: List<Int> = listOf(0)
 ): List<FingerprintSensorPropertiesInternal> {
-    val componentInfo = listOf(
+    val componentInfo =
+        listOf(
             ComponentInfoInternal(
-                    "fingerprintSensor" /* componentId */,
-                    "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */,
-                    "00000001" /* serialNumber */, "" /* softwareVersion */
+                "fingerprintSensor" /* componentId */,
+                "vendor/model/revision" /* hardwareVersion */,
+                "1.01" /* firmwareVersion */,
+                "00000001" /* serialNumber */,
+                "" /* softwareVersion */
             ),
             ComponentInfoInternal(
-                    "matchingAlgorithm" /* componentId */,
-                    "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */,
-                    "vendor/version/revision" /* softwareVersion */
+                "matchingAlgorithm" /* componentId */,
+                "" /* hardwareVersion */,
+                "" /* firmwareVersion */,
+                "" /* serialNumber */,
+                "vendor/version/revision" /* softwareVersion */
             )
-    )
+        )
     return ids.map { id ->
         FingerprintSensorPropertiesInternal(
-                id,
-                SensorProperties.STRENGTH_STRONG,
-                5 /* maxEnrollmentsPerUser */,
-                componentInfo,
-                FingerprintSensorProperties.TYPE_REAR,
-                false /* resetLockoutRequiresHardwareAuthToken */
+            id,
+            SensorProperties.STRENGTH_STRONG,
+            5 /* maxEnrollmentsPerUser */,
+            componentInfo,
+            FingerprintSensorProperties.TYPE_REAR,
+            false /* resetLockoutRequiresHardwareAuthToken */
         )
     }
 }
@@ -111,28 +115,53 @@
 internal fun faceSensorPropertiesInternal(
     ids: List<Int> = listOf(1)
 ): List<FaceSensorPropertiesInternal> {
-    val componentInfo = listOf(
+    val componentInfo =
+        listOf(
             ComponentInfoInternal(
-                    "faceSensor" /* componentId */,
-                    "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */,
-                    "00000001" /* serialNumber */, "" /* softwareVersion */
+                "faceSensor" /* componentId */,
+                "vendor/model/revision" /* hardwareVersion */,
+                "1.01" /* firmwareVersion */,
+                "00000001" /* serialNumber */,
+                "" /* softwareVersion */
             ),
             ComponentInfoInternal(
-                    "matchingAlgorithm" /* componentId */,
-                    "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */,
-                    "vendor/version/revision" /* softwareVersion */
+                "matchingAlgorithm" /* componentId */,
+                "" /* hardwareVersion */,
+                "" /* firmwareVersion */,
+                "" /* serialNumber */,
+                "vendor/version/revision" /* softwareVersion */
             )
-    )
+        )
     return ids.map { id ->
         FaceSensorPropertiesInternal(
-                id,
-                SensorProperties.STRENGTH_STRONG,
-                2 /* maxEnrollmentsPerUser */,
-                componentInfo,
-                FaceSensorProperties.TYPE_RGB,
-                true /* supportsFaceDetection */,
-                true /* supportsSelfIllumination */,
-                false /* resetLockoutRequiresHardwareAuthToken */
+            id,
+            SensorProperties.STRENGTH_STRONG,
+            2 /* maxEnrollmentsPerUser */,
+            componentInfo,
+            FaceSensorProperties.TYPE_RGB,
+            true /* supportsFaceDetection */,
+            true /* supportsSelfIllumination */,
+            false /* resetLockoutRequiresHardwareAuthToken */
         )
     }
 }
+
+internal fun promptInfo(
+    title: String = "title",
+    subtitle: String = "sub",
+    description: String = "desc",
+    credentialTitle: String? = "cred title",
+    credentialSubtitle: String? = "cred sub",
+    credentialDescription: String? = "cred desc",
+    negativeButton: String = "neg",
+): PromptInfo {
+    val info = PromptInfo()
+    info.title = title
+    info.subtitle = subtitle
+    info.description = description
+    credentialTitle?.let { info.deviceCredentialTitle = it }
+    credentialSubtitle?.let { info.deviceCredentialSubtitle = it }
+    credentialDescription?.let { info.deviceCredentialDescription = it }
+    info.negativeButtonText = negativeButton
+    return info
+}
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 49c6fd1..ed957db 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -69,6 +69,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -169,6 +170,8 @@
     private FakeExecutor mFgExecutor;
     @Mock
     private UdfpsDisplayMode mUdfpsDisplayMode;
+    @Mock
+    private FeatureFlags mFeatureFlags;
 
     // Stuff for configuring mocks
     @Mock
@@ -250,6 +253,7 @@
                 mStatusBarKeyguardViewManager,
                 mDumpManager,
                 mKeyguardUpdateMonitor,
+                mFeatureFlags,
                 mFalsingManager,
                 mPowerManager,
                 mAccessibilityManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt
new file mode 100644
index 0000000..2d5614c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt
@@ -0,0 +1,81 @@
+package com.android.systemui.biometrics.data.repository
+
+import android.hardware.biometrics.PromptInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.AuthController
+import com.android.systemui.biometrics.data.model.PromptKind
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(JUnit4::class)
+class PromptRepositoryImplTest : SysuiTestCase() {
+
+    @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var authController: AuthController
+
+    private lateinit var repository: PromptRepositoryImpl
+
+    @Before
+    fun setup() {
+        repository = PromptRepositoryImpl(authController)
+    }
+
+    @Test
+    fun isShowing() = runBlockingTest {
+        whenever(authController.isShowing).thenReturn(true)
+
+        val values = mutableListOf<Boolean>()
+        val job = launch { repository.isShowing.toList(values) }
+        assertThat(values).containsExactly(true)
+
+        withArgCaptor<AuthController.Callback> {
+            verify(authController).addCallback(capture())
+
+            value.onBiometricPromptShown()
+            assertThat(values).containsExactly(true, true)
+
+            value.onBiometricPromptDismissed()
+            assertThat(values).containsExactly(true, true, false).inOrder()
+
+            job.cancel()
+            verify(authController).removeCallback(eq(value))
+        }
+    }
+
+    @Test
+    fun setsAndUnsetsPrompt() = runBlockingTest {
+        val kind = PromptKind.PIN
+        val uid = 8
+        val challenge = 90L
+        val promptInfo = PromptInfo()
+
+        repository.setPrompt(promptInfo, uid, challenge, kind)
+
+        assertThat(repository.kind.value).isEqualTo(kind)
+        assertThat(repository.userId.value).isEqualTo(uid)
+        assertThat(repository.challenge.value).isEqualTo(challenge)
+        assertThat(repository.promptInfo.value).isSameInstanceAs(promptInfo)
+
+        repository.unsetPrompt()
+
+        assertThat(repository.promptInfo.value).isNull()
+        assertThat(repository.userId.value).isNull()
+        assertThat(repository.challenge.value).isNull()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
new file mode 100644
index 0000000..97d3e68
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
@@ -0,0 +1,216 @@
+package com.android.systemui.biometrics.domain.interactor
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResourcesManager
+import android.content.pm.UserInfo
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.internal.widget.LockscreenCredential
+import com.android.internal.widget.VerifyCredentialResponse
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.domain.model.BiometricOperationInfo
+import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
+import com.android.systemui.biometrics.domain.model.BiometricUserInfo
+import com.android.systemui.biometrics.promptInfo
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+
+private const val USER_ID = 22
+private const val OPERATION_ID = 100L
+private const val MAX_ATTEMPTS = 5
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class CredentialInteractorImplTest : SysuiTestCase() {
+
+    @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var lockPatternUtils: LockPatternUtils
+    @Mock private lateinit var userManager: UserManager
+    @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+    @Mock private lateinit var devicePolicyResourcesManager: DevicePolicyResourcesManager
+
+    private val systemClock = FakeSystemClock()
+
+    private lateinit var interactor: CredentialInteractorImpl
+
+    @Before
+    fun setup() {
+        whenever(devicePolicyManager.resources).thenReturn(devicePolicyResourcesManager)
+        whenever(lockPatternUtils.getMaximumFailedPasswordsForWipe(anyInt()))
+            .thenReturn(MAX_ATTEMPTS)
+        whenever(userManager.getUserInfo(eq(USER_ID))).thenReturn(UserInfo(USER_ID, "", 0))
+        whenever(devicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(eq(USER_ID)))
+            .thenReturn(USER_ID)
+
+        interactor =
+            CredentialInteractorImpl(
+                mContext,
+                lockPatternUtils,
+                userManager,
+                devicePolicyManager,
+                systemClock
+            )
+    }
+
+    @Test
+    fun testStealthMode() {
+        for (value in listOf(true, false, false, true)) {
+            whenever(lockPatternUtils.isVisiblePatternEnabled(eq(USER_ID))).thenReturn(value)
+
+            assertThat(interactor.isStealthModeActive(USER_ID)).isEqualTo(!value)
+        }
+    }
+
+    @Test
+    fun testCredentialOwner() {
+        for (value in listOf(12, 8, 4)) {
+            whenever(userManager.getCredentialOwnerProfile(eq(USER_ID))).thenReturn(value)
+
+            assertThat(interactor.getCredentialOwnerOrSelfId(USER_ID)).isEqualTo(value)
+        }
+    }
+
+    @Test fun pinCredentialWhenGood() = pinCredential(goodCredential())
+
+    @Test fun pinCredentialWhenBad() = pinCredential(badCredential())
+
+    @Test fun pinCredentialWhenBadAndThrottled() = pinCredential(badCredential(timeout = 5_000))
+
+    private fun pinCredential(result: VerifyCredentialResponse) = runTest {
+        val usedAttempts = 1
+        whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID)))
+            .thenReturn(usedAttempts)
+        whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt())).thenReturn(result)
+        whenever(lockPatternUtils.verifyGatekeeperPasswordHandle(anyLong(), anyLong(), eq(USER_ID)))
+            .thenReturn(result)
+        whenever(lockPatternUtils.setLockoutAttemptDeadline(anyInt(), anyInt())).thenAnswer {
+            systemClock.elapsedRealtime() + (it.arguments[1] as Int)
+        }
+
+        // wrap in an async block so the test can advance the clock if throttling credential
+        // checks prevents the method from returning
+        val statusList = mutableListOf<CredentialStatus>()
+        interactor
+            .verifyCredential(pinRequest(), LockscreenCredential.createPin("1234"))
+            .toList(statusList)
+
+        val last = statusList.removeLastOrNull()
+        if (result.isMatched) {
+            assertThat(statusList).isEmpty()
+            val successfulResult = last as? CredentialStatus.Success.Verified
+            assertThat(successfulResult).isNotNull()
+            assertThat(successfulResult!!.hat).isEqualTo(result.gatekeeperHAT)
+
+            verify(lockPatternUtils).userPresent(eq(USER_ID))
+            verify(lockPatternUtils)
+                .removeGatekeeperPasswordHandle(eq(result.gatekeeperPasswordHandle))
+        } else {
+            val failedResult = last as? CredentialStatus.Fail.Error
+            assertThat(failedResult).isNotNull()
+            assertThat(failedResult!!.remainingAttempts)
+                .isEqualTo(if (result.timeout > 0) null else MAX_ATTEMPTS - usedAttempts - 1)
+            assertThat(failedResult.urgentMessage).isNull()
+
+            if (result.timeout > 0) { // failed and throttled
+                // messages are in the throttled errors, so the final Error.error is empty
+                assertThat(failedResult.error).isEmpty()
+                assertThat(statusList).isNotEmpty()
+                assertThat(statusList.filterIsInstance(CredentialStatus.Fail.Throttled::class.java))
+                    .hasSize(statusList.size)
+
+                verify(lockPatternUtils).setLockoutAttemptDeadline(eq(USER_ID), eq(result.timeout))
+            } else { // failed
+                assertThat(failedResult.error)
+                    .matches(Regex("(.*)try again(.*)", RegexOption.IGNORE_CASE).toPattern())
+                assertThat(statusList).isEmpty()
+
+                verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID))
+            }
+        }
+    }
+
+    @Test
+    fun pinCredentialWhenBadAndFinalAttempt() = runTest {
+        whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt()))
+            .thenReturn(badCredential())
+        whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID)))
+            .thenReturn(MAX_ATTEMPTS - 2)
+
+        val statusList = mutableListOf<CredentialStatus>()
+        interactor
+            .verifyCredential(pinRequest(), LockscreenCredential.createPin("1234"))
+            .toList(statusList)
+
+        val result = statusList.removeLastOrNull() as? CredentialStatus.Fail.Error
+        assertThat(result).isNotNull()
+        assertThat(result!!.remainingAttempts).isEqualTo(1)
+        assertThat(result.urgentMessage).isNotEmpty()
+        assertThat(statusList).isEmpty()
+
+        verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID))
+    }
+
+    @Test
+    fun pinCredentialWhenBadAndNoMoreAttempts() = runTest {
+        whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt()))
+            .thenReturn(badCredential())
+        whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID)))
+            .thenReturn(MAX_ATTEMPTS - 1)
+        whenever(devicePolicyResourcesManager.getString(any(), any())).thenReturn("wipe")
+
+        val statusList = mutableListOf<CredentialStatus>()
+        interactor
+            .verifyCredential(pinRequest(), LockscreenCredential.createPin("1234"))
+            .toList(statusList)
+
+        val result = statusList.removeLastOrNull() as? CredentialStatus.Fail.Error
+        assertThat(result).isNotNull()
+        assertThat(result!!.remainingAttempts).isEqualTo(0)
+        assertThat(result.urgentMessage).isNotEmpty()
+        assertThat(statusList).isEmpty()
+
+        verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID))
+    }
+}
+
+private fun pinRequest(): BiometricPromptRequest.Credential.Pin =
+    BiometricPromptRequest.Credential.Pin(
+        promptInfo(),
+        BiometricUserInfo(USER_ID),
+        BiometricOperationInfo(OPERATION_ID)
+    )
+
+private fun goodCredential(
+    passwordHandle: Long = 90,
+    hat: ByteArray = ByteArray(69),
+): VerifyCredentialResponse =
+    VerifyCredentialResponse.Builder()
+        .setGatekeeperPasswordHandle(passwordHandle)
+        .setGatekeeperHAT(hat)
+        .build()
+
+private fun badCredential(timeout: Int = 0): VerifyCredentialResponse =
+    if (timeout > 0) {
+        VerifyCredentialResponse.fromTimeout(timeout)
+    } else {
+        VerifyCredentialResponse.fromError()
+    }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
new file mode 100644
index 0000000..dbcbf41
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
@@ -0,0 +1,270 @@
+package com.android.systemui.biometrics.domain.interactor
+
+import android.hardware.biometrics.PromptInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.data.repository.FakePromptRepository
+import com.android.systemui.biometrics.domain.model.BiometricOperationInfo
+import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
+import com.android.systemui.biometrics.domain.model.BiometricUserInfo
+import com.android.systemui.biometrics.promptInfo
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.junit.MockitoJUnit
+
+private const val USER_ID = 22
+private const val OPERATION_ID = 100L
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class PromptCredentialInteractorTest : SysuiTestCase() {
+
+    @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
+
+    private val dispatcher = UnconfinedTestDispatcher()
+    private val biometricPromptRepository = FakePromptRepository()
+    private val credentialInteractor = FakeCredentialInteractor()
+
+    private lateinit var interactor: BiometricPromptCredentialInteractor
+
+    @Before
+    fun setup() {
+        interactor =
+            BiometricPromptCredentialInteractor(
+                dispatcher,
+                biometricPromptRepository,
+                credentialInteractor
+            )
+    }
+
+    @Test
+    fun testIsShowing() =
+        runTest(dispatcher) {
+            var showing = false
+            val job = launch { interactor.isShowing.collect { showing = it } }
+
+            biometricPromptRepository.setIsShowing(false)
+            assertThat(showing).isFalse()
+
+            biometricPromptRepository.setIsShowing(true)
+            assertThat(showing).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun testShowError() =
+        runTest(dispatcher) {
+            var error: CredentialStatus.Fail? = null
+            val job = launch { interactor.verificationError.collect { error = it } }
+
+            for (msg in listOf("once", "again")) {
+                interactor.setVerificationError(error(msg))
+                assertThat(error).isEqualTo(error(msg))
+            }
+
+            interactor.resetVerificationError()
+            assertThat(error).isNull()
+
+            job.cancel()
+        }
+
+    @Test
+    fun nullWhenNoPromptInfo() =
+        runTest(dispatcher) {
+            var prompt: BiometricPromptRequest? = null
+            val job = launch { interactor.prompt.collect { prompt = it } }
+
+            assertThat(prompt).isNull()
+
+            job.cancel()
+        }
+
+    @Test fun usePinCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PIN)
+
+    @Test fun usePasswordCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PASSWORD)
+
+    @Test fun usePatternCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PATTERN)
+
+    private fun useCredentialForPrompt(kind: Int) =
+        runTest(dispatcher) {
+            val isStealth = false
+            credentialInteractor.stealthMode = isStealth
+
+            var prompt: BiometricPromptRequest? = null
+            val job = launch { interactor.prompt.collect { prompt = it } }
+
+            val title = "what a prompt"
+            val subtitle = "s"
+            val description = "something to see"
+
+            interactor.useCredentialsForAuthentication(
+                PromptInfo().also {
+                    it.title = title
+                    it.description = description
+                    it.subtitle = subtitle
+                },
+                kind = kind,
+                userId = USER_ID,
+                challenge = OPERATION_ID
+            )
+
+            val p = prompt as? BiometricPromptRequest.Credential
+            assertThat(p).isNotNull()
+            assertThat(p!!.title).isEqualTo(title)
+            assertThat(p.subtitle).isEqualTo(subtitle)
+            assertThat(p.description).isEqualTo(description)
+            assertThat(p.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
+            assertThat(p.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
+            assertThat(p)
+                .isInstanceOf(
+                    when (kind) {
+                        Utils.CREDENTIAL_PIN -> BiometricPromptRequest.Credential.Pin::class.java
+                        Utils.CREDENTIAL_PASSWORD ->
+                            BiometricPromptRequest.Credential.Password::class.java
+                        Utils.CREDENTIAL_PATTERN ->
+                            BiometricPromptRequest.Credential.Pattern::class.java
+                        else -> throw Exception("wrong kind")
+                    }
+                )
+            if (p is BiometricPromptRequest.Credential.Pattern) {
+                assertThat(p.stealthMode).isEqualTo(isStealth)
+            }
+
+            interactor.resetPrompt()
+
+            assertThat(prompt).isNull()
+
+            job.cancel()
+        }
+
+    @Test
+    fun checkCredential() =
+        runTest(dispatcher) {
+            val hat = ByteArray(4)
+            credentialInteractor.verifyCredentialResponse = { _ -> flowOf(verified(hat)) }
+
+            val errors = mutableListOf<CredentialStatus.Fail?>()
+            val job = launch { interactor.verificationError.toList(errors) }
+
+            val checked =
+                interactor.checkCredential(pinRequest(), text = "1234")
+                    as? CredentialStatus.Success.Verified
+
+            assertThat(checked).isNotNull()
+            assertThat(checked!!.hat).isSameInstanceAs(hat)
+            assertThat(errors.map { it?.error }).containsExactly(null)
+
+            job.cancel()
+        }
+
+    @Test
+    fun checkCredentialWhenBad() =
+        runTest(dispatcher) {
+            val errorMessage = "bad"
+            val remainingAttempts = 12
+            credentialInteractor.verifyCredentialResponse = { _ ->
+                flowOf(error(errorMessage, remainingAttempts))
+            }
+
+            val errors = mutableListOf<CredentialStatus.Fail?>()
+            val job = launch { interactor.verificationError.toList(errors) }
+
+            val checked =
+                interactor.checkCredential(pinRequest(), text = "1234")
+                    as? CredentialStatus.Fail.Error
+
+            assertThat(checked).isNotNull()
+            assertThat(checked!!.remainingAttempts).isEqualTo(remainingAttempts)
+            assertThat(checked.urgentMessage).isNull()
+            assertThat(errors.map { it?.error }).containsExactly(null, errorMessage).inOrder()
+
+            job.cancel()
+        }
+
+    @Test
+    fun checkCredentialWhenBadAndUrgentMessage() =
+        runTest(dispatcher) {
+            val error = "not so bad"
+            val urgentMessage = "really bad"
+            credentialInteractor.verifyCredentialResponse = { _ ->
+                flowOf(error(error, 10, urgentMessage))
+            }
+
+            val errors = mutableListOf<CredentialStatus.Fail?>()
+            val job = launch { interactor.verificationError.toList(errors) }
+
+            val checked =
+                interactor.checkCredential(pinRequest(), text = "1234")
+                    as? CredentialStatus.Fail.Error
+
+            assertThat(checked).isNotNull()
+            assertThat(checked!!.urgentMessage).isEqualTo(urgentMessage)
+            assertThat(errors.map { it?.error }).containsExactly(null, error).inOrder()
+            assertThat(errors.last() as? CredentialStatus.Fail.Error)
+                .isEqualTo(error(error, 10, urgentMessage))
+
+            job.cancel()
+        }
+
+    @Test
+    fun checkCredentialWhenBadAndThrottled() =
+        runTest(dispatcher) {
+            val remainingAttempts = 3
+            val error = ":("
+            val urgentMessage = ":D"
+            credentialInteractor.verifyCredentialResponse = { _ ->
+                flow {
+                    for (i in 1..3) {
+                        emit(throttled("$i"))
+                        delay(100)
+                    }
+                    emit(error(error, remainingAttempts, urgentMessage))
+                }
+            }
+            val errors = mutableListOf<CredentialStatus.Fail?>()
+            val job = launch { interactor.verificationError.toList(errors) }
+
+            val checked =
+                interactor.checkCredential(pinRequest(), text = "1234")
+                    as? CredentialStatus.Fail.Error
+
+            assertThat(checked).isNotNull()
+            assertThat(checked!!.remainingAttempts).isEqualTo(remainingAttempts)
+            assertThat(checked.urgentMessage).isEqualTo(urgentMessage)
+            assertThat(errors.map { it?.error })
+                .containsExactly(null, "1", "2", "3", error)
+                .inOrder()
+
+            job.cancel()
+        }
+}
+
+private fun pinRequest(): BiometricPromptRequest.Credential.Pin =
+    BiometricPromptRequest.Credential.Pin(
+        promptInfo(),
+        BiometricUserInfo(USER_ID),
+        BiometricOperationInfo(OPERATION_ID)
+    )
+
+private fun verified(hat: ByteArray) = CredentialStatus.Success.Verified(hat)
+
+private fun throttled(error: String) = CredentialStatus.Fail.Throttled(error)
+
+private fun error(error: String? = null, remaining: Int? = null, urgentMessage: String? = null) =
+    CredentialStatus.Fail.Error(error, remaining, urgentMessage)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
new file mode 100644
index 0000000..4c5e3c1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
@@ -0,0 +1,93 @@
+package com.android.systemui.biometrics.domain.model
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.promptInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+private const val USER_ID = 2
+private const val OPERATION_ID = 8L
+
+@SmallTest
+@RunWith(JUnit4::class)
+class BiometricPromptRequestTest : SysuiTestCase() {
+
+    @Test
+    fun biometricRequestFromPromptInfo() {
+        val title = "what"
+        val subtitle = "a"
+        val description = "request"
+
+        val request =
+            BiometricPromptRequest.Biometric(
+                promptInfo(title = title, subtitle = subtitle, description = description),
+                BiometricUserInfo(USER_ID),
+                BiometricOperationInfo(OPERATION_ID)
+            )
+
+        assertThat(request.title).isEqualTo(title)
+        assertThat(request.subtitle).isEqualTo(subtitle)
+        assertThat(request.description).isEqualTo(description)
+        assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
+        assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
+    }
+
+    @Test
+    fun credentialRequestFromPromptInfo() {
+        val title = "what"
+        val subtitle = "a"
+        val description = "request"
+        val stealth = true
+
+        val toCheck =
+            listOf(
+                BiometricPromptRequest.Credential.Pin(
+                    promptInfo(
+                        title = title,
+                        subtitle = subtitle,
+                        description = description,
+                        credentialTitle = null,
+                        credentialSubtitle = null,
+                        credentialDescription = null
+                    ),
+                    BiometricUserInfo(USER_ID),
+                    BiometricOperationInfo(OPERATION_ID)
+                ),
+                BiometricPromptRequest.Credential.Password(
+                    promptInfo(
+                        credentialTitle = title,
+                        credentialSubtitle = subtitle,
+                        credentialDescription = description
+                    ),
+                    BiometricUserInfo(USER_ID),
+                    BiometricOperationInfo(OPERATION_ID)
+                ),
+                BiometricPromptRequest.Credential.Pattern(
+                    promptInfo(
+                        subtitle = subtitle,
+                        description = description,
+                        credentialTitle = title,
+                        credentialSubtitle = null,
+                        credentialDescription = null
+                    ),
+                    BiometricUserInfo(USER_ID),
+                    BiometricOperationInfo(OPERATION_ID),
+                    stealth
+                )
+            )
+
+        for (request in toCheck) {
+            assertThat(request.title).isEqualTo(title)
+            assertThat(request.subtitle).isEqualTo(subtitle)
+            assertThat(request.description).isEqualTo(description)
+            assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
+            assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
+            if (request is BiometricPromptRequest.Credential.Pattern) {
+                assertThat(request.stealthMode).isEqualTo(stealth)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt
new file mode 100644
index 0000000..d73cdfc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt
@@ -0,0 +1,181 @@
+package com.android.systemui.biometrics.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.model.PromptKind
+import com.android.systemui.biometrics.data.repository.FakePromptRepository
+import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.CredentialStatus
+import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor
+import com.android.systemui.biometrics.promptInfo
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+private const val USER_ID = 9
+private const val OPERATION_ID = 10L
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class CredentialViewModelTest : SysuiTestCase() {
+
+    private val dispatcher = UnconfinedTestDispatcher()
+    private val promptRepository = FakePromptRepository()
+    private val credentialInteractor = FakeCredentialInteractor()
+
+    private lateinit var viewModel: CredentialViewModel
+
+    @Before
+    fun setup() {
+        viewModel =
+            CredentialViewModel(
+                mContext,
+                BiometricPromptCredentialInteractor(
+                    dispatcher,
+                    promptRepository,
+                    credentialInteractor
+                )
+            )
+    }
+
+    @Test fun setsPinInputFlags() = setsInputFlags(PromptKind.PIN, expectFlags = true)
+    @Test fun setsPasswordInputFlags() = setsInputFlags(PromptKind.PASSWORD, expectFlags = false)
+    @Test fun setsPatternInputFlags() = setsInputFlags(PromptKind.PATTERN, expectFlags = false)
+
+    private fun setsInputFlags(type: PromptKind, expectFlags: Boolean) =
+        runTestWithKind(type) {
+            var flags: Int? = null
+            val job = launch { viewModel.inputFlags.collect { flags = it } }
+
+            if (expectFlags) {
+                assertThat(flags).isNotNull()
+            } else {
+                assertThat(flags).isNull()
+            }
+            job.cancel()
+        }
+
+    @Test fun isStealthIgnoredByPin() = isStealthMode(PromptKind.PIN, expectStealth = false)
+    @Test
+    fun isStealthIgnoredByPassword() = isStealthMode(PromptKind.PASSWORD, expectStealth = false)
+    @Test fun isStealthUsedByPattern() = isStealthMode(PromptKind.PATTERN, expectStealth = true)
+
+    private fun isStealthMode(type: PromptKind, expectStealth: Boolean) =
+        runTestWithKind(type, init = { credentialInteractor.stealthMode = true }) {
+            var stealth: Boolean? = null
+            val job = launch { viewModel.stealthMode.collect { stealth = it } }
+
+            assertThat(stealth).isEqualTo(expectStealth)
+
+            job.cancel()
+        }
+
+    @Test
+    fun animatesContents() = runTestWithKind {
+        val expected = arrayOf(true, false, true)
+        val animate = mutableListOf<Boolean>()
+        val job = launch { viewModel.animateContents.toList(animate) }
+
+        for (value in expected) {
+            viewModel.setAnimateContents(value)
+            viewModel.setAnimateContents(value)
+        }
+        assertThat(animate).containsExactly(*expected).inOrder()
+
+        job.cancel()
+    }
+
+    @Test
+    fun showAndClearErrors() = runTestWithKind {
+        var error = ""
+        val job = launch { viewModel.errorMessage.collect { error = it } }
+        assertThat(error).isEmpty()
+
+        viewModel.showPatternTooShortError()
+        assertThat(error).isNotEmpty()
+
+        viewModel.resetErrorMessage()
+        assertThat(error).isEmpty()
+
+        job.cancel()
+    }
+
+    @Test
+    fun checkCredential() = runTestWithKind {
+        val hat = ByteArray(2)
+        credentialInteractor.verifyCredentialResponse = { _ ->
+            flowOf(CredentialStatus.Success.Verified(hat))
+        }
+
+        val attestations = mutableListOf<ByteArray?>()
+        val remainingAttempts = mutableListOf<RemainingAttempts?>()
+        var header: HeaderViewModel? = null
+        val job = launch {
+            launch { viewModel.validatedAttestation.toList(attestations) }
+            launch { viewModel.remainingAttempts.toList(remainingAttempts) }
+            launch { viewModel.header.collect { header = it } }
+        }
+        assertThat(header).isNotNull()
+
+        viewModel.checkCredential("p", header!!)
+
+        val attestation = attestations.removeLastOrNull()
+        assertThat(attestation).isSameInstanceAs(hat)
+        assertThat(attestations).isEmpty()
+        assertThat(remainingAttempts).containsExactly(RemainingAttempts())
+
+        job.cancel()
+    }
+
+    @Test
+    fun checkCredentialWhenBad() = runTestWithKind {
+        val remaining = 2
+        val urgentError = "wow"
+        credentialInteractor.verifyCredentialResponse = { _ ->
+            flowOf(CredentialStatus.Fail.Error("error", remaining, urgentError))
+        }
+
+        val attestations = mutableListOf<ByteArray?>()
+        val remainingAttempts = mutableListOf<RemainingAttempts?>()
+        var header: HeaderViewModel? = null
+        val job = launch {
+            launch { viewModel.validatedAttestation.toList(attestations) }
+            launch { viewModel.remainingAttempts.toList(remainingAttempts) }
+            launch { viewModel.header.collect { header = it } }
+        }
+        assertThat(header).isNotNull()
+
+        viewModel.checkCredential("1111", header!!)
+
+        assertThat(attestations).containsExactly(null)
+
+        val attemptInfo = remainingAttempts.removeLastOrNull()
+        assertThat(attemptInfo).isNotNull()
+        assertThat(attemptInfo!!.remaining).isEqualTo(remaining)
+        assertThat(attemptInfo.message).isEqualTo(urgentError)
+        assertThat(remainingAttempts).containsExactly(RemainingAttempts()) // initial value
+
+        job.cancel()
+    }
+
+    private fun runTestWithKind(
+        kind: PromptKind = PromptKind.PIN,
+        init: () -> Unit = {},
+        block: suspend TestScope.() -> Unit,
+    ) =
+        runTest(dispatcher) {
+            init()
+            promptRepository.setPrompt(promptInfo(), USER_ID, OPERATION_ID, kind)
+            block()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt
index 20a82c6..4b3b70e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt
@@ -88,6 +88,7 @@
             flagMap,
             restarter
         )
+        mFeatureFlagsDebug.init()
         verify(flagManager).onSettingsChangedAction = any()
         broadcastReceiver = withArgCaptor {
             verify(mockContext).registerReceiver(capture(), any(), nullable(), nullable(),
@@ -255,11 +256,11 @@
         broadcastReceiver.onReceive(mockContext, Intent())
         broadcastReceiver.onReceive(mockContext, Intent("invalid action"))
         broadcastReceiver.onReceive(mockContext, Intent(FlagManager.ACTION_SET_FLAG))
-        setByBroadcast(0, false)     // unknown id does nothing
-        setByBroadcast(1, "string")  // wrong type does nothing
-        setByBroadcast(2, 123)       // wrong type does nothing
-        setByBroadcast(3, false)     // wrong type does nothing
-        setByBroadcast(4, 123)       // wrong type does nothing
+        setByBroadcast(0, false) // unknown id does nothing
+        setByBroadcast(1, "string") // wrong type does nothing
+        setByBroadcast(2, 123) // wrong type does nothing
+        setByBroadcast(3, false) // wrong type does nothing
+        setByBroadcast(4, 123) // wrong type does nothing
         verifyNoMoreInteractions(flagManager, secureSettings)
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt
index 575c142..b2dd60c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt
@@ -38,8 +38,9 @@
 
     @Mock private lateinit var mResources: Resources
     @Mock private lateinit var mSystemProperties: SystemPropertiesHelper
+    @Mock private lateinit var restarter: Restarter
+    private val flagMap = mutableMapOf<Int, Flag<*>>()
     private val serverFlagReader = ServerFlagReaderFake()
-
     private val deviceConfig = DeviceConfigProxyFake()
 
     @Before
@@ -49,7 +50,9 @@
             mResources,
             mSystemProperties,
             deviceConfig,
-            serverFlagReader)
+            serverFlagReader,
+            flagMap,
+            restarter)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/ServerFlagReaderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/ServerFlagReaderImplTest.kt
new file mode 100644
index 0000000..6f5f460
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/ServerFlagReaderImplTest.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.flags
+
+import android.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.DeviceConfigProxyFake
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ServerFlagReaderImplTest : SysuiTestCase() {
+
+    private val NAMESPACE = "test"
+
+    @Mock private lateinit var changeListener: ServerFlagReader.ChangeListener
+
+    private lateinit var serverFlagReader: ServerFlagReaderImpl
+    private val deviceConfig = DeviceConfigProxyFake()
+    private val executor = FakeExecutor(FakeSystemClock())
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        serverFlagReader = ServerFlagReaderImpl(NAMESPACE, deviceConfig, executor)
+    }
+
+    @Test
+    fun testChange_alertsListener() {
+        val flag = ReleasedFlag(1)
+        serverFlagReader.listenForChanges(listOf(flag), changeListener)
+
+        deviceConfig.setProperty(NAMESPACE, "flag_override_1", "1", false)
+        executor.runAllReady()
+
+        verify(changeListener).onChange()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
similarity index 62%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
index e99c139..f18acba 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
@@ -15,10 +15,10 @@
  *
  */
 
-package com.android.systemui.keyguard.domain.quickaffordance
+package com.android.systemui.keyguard.data.quickaffordance
 
 import com.android.systemui.animation.Expandable
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.yield
@@ -29,24 +29,27 @@
  * This class is abstract to force tests to provide extensions of it as the system that references
  * these configs uses each implementation's class type to refer to them.
  */
-abstract class FakeKeyguardQuickAffordanceConfig : KeyguardQuickAffordanceConfig {
+abstract class FakeKeyguardQuickAffordanceConfig(
+    override val key: String,
+) : KeyguardQuickAffordanceConfig {
 
-    var onClickedResult: OnClickedResult = OnClickedResult.Handled
+    var onTriggeredResult: OnTriggeredResult = OnTriggeredResult.Handled
 
-    private val _state =
-        MutableStateFlow<KeyguardQuickAffordanceConfig.State>(
-            KeyguardQuickAffordanceConfig.State.Hidden
+    private val _lockScreenState =
+        MutableStateFlow<KeyguardQuickAffordanceConfig.LockScreenState>(
+            KeyguardQuickAffordanceConfig.LockScreenState.Hidden
         )
-    override val state: Flow<KeyguardQuickAffordanceConfig.State> = _state
+    override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
+        _lockScreenState
 
-    override fun onQuickAffordanceClicked(
+    override fun onTriggered(
         expandable: Expandable?,
-    ): OnClickedResult {
-        return onClickedResult
+    ): OnTriggeredResult {
+        return onTriggeredResult
     }
 
-    suspend fun setState(state: KeyguardQuickAffordanceConfig.State) {
-        _state.value = state
+    suspend fun setState(lockScreenState: KeyguardQuickAffordanceConfig.LockScreenState) {
+        _lockScreenState.value = lockScreenState
         // Yield to allow the test's collection coroutine to "catch up" and collect this value
         // before the test continues to the next line.
         // TODO(b/239834928): once coroutines.test is updated, switch to the approach described in
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
similarity index 83%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
index 9a91ea91..c94cec6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
@@ -1,21 +1,21 @@
 /*
- *  Copyright (C) 2022 The Android Open Source Project
+ * 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
+ * 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
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
  *
  */
 
-package com.android.systemui.keyguard.domain.quickaffordance
+package com.android.systemui.keyguard.data.quickaffordance
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
@@ -122,8 +122,8 @@
                     emptyList()
                 }
             )
-        val values = mutableListOf<KeyguardQuickAffordanceConfig.State>()
-        val job = underTest.state.onEach(values::add).launchIn(this)
+        val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+        val job = underTest.lockScreenState.onEach(values::add).launchIn(this)
 
         if (canShowWhileLocked) {
             verify(controlsListingController).addCallback(callbackCaptor.capture())
@@ -139,9 +139,9 @@
         assertThat(values.last())
             .isInstanceOf(
                 if (isVisibleExpected) {
-                    KeyguardQuickAffordanceConfig.State.Visible::class.java
+                    KeyguardQuickAffordanceConfig.LockScreenState.Visible::class.java
                 } else {
-                    KeyguardQuickAffordanceConfig.State.Hidden::class.java
+                    KeyguardQuickAffordanceConfig.LockScreenState.Hidden::class.java
                 }
             )
         job.cancel()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
similarity index 65%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
index a809f05..659c1e5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
@@ -1,21 +1,21 @@
 /*
- *  Copyright (C) 2022 The Android Open Source Project
+ * 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
+ * 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
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
  *
  */
 
-package com.android.systemui.keyguard.domain.quickaffordance
+package com.android.systemui.keyguard.data.quickaffordance
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
@@ -23,7 +23,7 @@
 import com.android.systemui.animation.Expandable
 import com.android.systemui.controls.controller.ControlsController
 import com.android.systemui.controls.dagger.ControlsComponent
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import java.util.Optional
@@ -72,11 +72,11 @@
         whenever(component.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE)
         whenever(controlsController.getFavorites()).thenReturn(listOf(mock()))
 
-        val values = mutableListOf<KeyguardQuickAffordanceConfig.State>()
-        val job = underTest.state.onEach(values::add).launchIn(this)
+        val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+        val job = underTest.lockScreenState.onEach(values::add).launchIn(this)
 
         assertThat(values.last())
-            .isInstanceOf(KeyguardQuickAffordanceConfig.State.Hidden::class.java)
+            .isInstanceOf(KeyguardQuickAffordanceConfig.LockScreenState.Hidden::class.java)
         job.cancel()
     }
 
@@ -91,31 +91,32 @@
         whenever(component.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE)
         whenever(controlsController.getFavorites()).thenReturn(listOf(mock()))
 
-        val values = mutableListOf<KeyguardQuickAffordanceConfig.State>()
-        val job = underTest.state.onEach(values::add).launchIn(this)
+        val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
+        val job = underTest.lockScreenState.onEach(values::add).launchIn(this)
 
         assertThat(values.last())
-            .isInstanceOf(KeyguardQuickAffordanceConfig.State.Hidden::class.java)
+            .isInstanceOf(KeyguardQuickAffordanceConfig.LockScreenState.Hidden::class.java)
         job.cancel()
     }
 
     @Test
-    fun `onQuickAffordanceClicked - canShowWhileLockedSetting is true`() = runBlockingTest {
+    fun `onQuickAffordanceTriggered - canShowWhileLockedSetting is true`() = runBlockingTest {
         whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(true))
 
-        val onClickedResult = underTest.onQuickAffordanceClicked(expandable)
+        val onClickedResult = underTest.onTriggered(expandable)
 
-        assertThat(onClickedResult).isInstanceOf(OnClickedResult.StartActivity::class.java)
-        assertThat((onClickedResult as OnClickedResult.StartActivity).canShowWhileLocked).isTrue()
+        assertThat(onClickedResult).isInstanceOf(OnTriggeredResult.StartActivity::class.java)
+        assertThat((onClickedResult as OnTriggeredResult.StartActivity).canShowWhileLocked).isTrue()
     }
 
     @Test
-    fun `onQuickAffordanceClicked - canShowWhileLockedSetting is false`() = runBlockingTest {
+    fun `onQuickAffordanceTriggered - canShowWhileLockedSetting is false`() = runBlockingTest {
         whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(false))
 
-        val onClickedResult = underTest.onQuickAffordanceClicked(expandable)
+        val onClickedResult = underTest.onTriggered(expandable)
 
-        assertThat(onClickedResult).isInstanceOf(OnClickedResult.StartActivity::class.java)
-        assertThat((onClickedResult as OnClickedResult.StartActivity).canShowWhileLocked).isFalse()
+        assertThat(onClickedResult).isInstanceOf(OnTriggeredResult.StartActivity::class.java)
+        assertThat((onClickedResult as OnTriggeredResult.StartActivity).canShowWhileLocked)
+            .isFalse()
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
similarity index 70%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
index 329c4db..61a3f9f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
@@ -1,26 +1,26 @@
 /*
- *  Copyright (C) 2022 The Android Open Source Project
+ * 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
+ * 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
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
  *
  */
 
-package com.android.systemui.keyguard.domain.quickaffordance
+package com.android.systemui.keyguard.data.quickaffordance
 
 import android.content.Intent
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult
 import com.android.systemui.qrcodescanner.controller.QRCodeScannerController
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.mock
@@ -56,9 +56,9 @@
     @Test
     fun `affordance - sets up registration and delivers initial model`() = runBlockingTest {
         whenever(controller.isEnabledForLockScreenButton).thenReturn(true)
-        var latest: KeyguardQuickAffordanceConfig.State? = null
+        var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
 
-        val job = underTest.state.onEach { latest = it }.launchIn(this)
+        val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
 
         val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>()
         verify(controller).addCallback(callbackCaptor.capture())
@@ -77,8 +77,8 @@
     fun `affordance - scanner activity changed - delivers model with updated intent`() =
         runBlockingTest {
             whenever(controller.isEnabledForLockScreenButton).thenReturn(true)
-            var latest: KeyguardQuickAffordanceConfig.State? = null
-            val job = underTest.state.onEach { latest = it }.launchIn(this)
+            var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
+            val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
             val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>()
             verify(controller).addCallback(callbackCaptor.capture())
 
@@ -93,8 +93,8 @@
 
     @Test
     fun `affordance - scanner preference changed - delivers visible model`() = runBlockingTest {
-        var latest: KeyguardQuickAffordanceConfig.State? = null
-        val job = underTest.state.onEach { latest = it }.launchIn(this)
+        var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
+        val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
         val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>()
         verify(controller).addCallback(callbackCaptor.capture())
 
@@ -109,34 +109,35 @@
 
     @Test
     fun `affordance - scanner preference changed - delivers none`() = runBlockingTest {
-        var latest: KeyguardQuickAffordanceConfig.State? = null
-        val job = underTest.state.onEach { latest = it }.launchIn(this)
+        var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
+        val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
         val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>()
         verify(controller).addCallback(callbackCaptor.capture())
 
         whenever(controller.isEnabledForLockScreenButton).thenReturn(false)
         callbackCaptor.value.onQRCodeScannerPreferenceChanged()
 
-        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden)
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
 
         job.cancel()
         verify(controller).removeCallback(callbackCaptor.value)
     }
 
     @Test
-    fun onQuickAffordanceClicked() {
-        assertThat(underTest.onQuickAffordanceClicked(mock()))
+    fun onQuickAffordanceTriggered() {
+        assertThat(underTest.onTriggered(mock()))
             .isEqualTo(
-                OnClickedResult.StartActivity(
+                OnTriggeredResult.StartActivity(
                     intent = INTENT_1,
                     canShowWhileLocked = true,
                 )
             )
     }
 
-    private fun assertVisibleState(latest: KeyguardQuickAffordanceConfig.State?) {
-        assertThat(latest).isInstanceOf(KeyguardQuickAffordanceConfig.State.Visible::class.java)
-        val visibleState = latest as KeyguardQuickAffordanceConfig.State.Visible
+    private fun assertVisibleState(latest: KeyguardQuickAffordanceConfig.LockScreenState?) {
+        assertThat(latest)
+            .isInstanceOf(KeyguardQuickAffordanceConfig.LockScreenState.Visible::class.java)
+        val visibleState = latest as KeyguardQuickAffordanceConfig.LockScreenState.Visible
         assertThat(visibleState.icon).isNotNull()
         assertThat(visibleState.icon.contentDescription).isNotNull()
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
similarity index 74%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
index 98dc4c4..c05beef 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
@@ -1,21 +1,21 @@
 /*
- *  Copyright (C) 2022 The Android Open Source Project
+ * 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
+ * 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
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
  *
  */
 
-package com.android.systemui.keyguard.domain.quickaffordance
+package com.android.systemui.keyguard.data.quickaffordance
 
 import android.graphics.drawable.Drawable
 import android.service.quickaccesswallet.GetWalletCardsResponse
@@ -67,11 +67,11 @@
     @Test
     fun `affordance - keyguard showing - has wallet card - visible model`() = runBlockingTest {
         setUpState()
-        var latest: KeyguardQuickAffordanceConfig.State? = null
+        var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
 
-        val job = underTest.state.onEach { latest = it }.launchIn(this)
+        val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
 
-        val visibleModel = latest as KeyguardQuickAffordanceConfig.State.Visible
+        val visibleModel = latest as KeyguardQuickAffordanceConfig.LockScreenState.Visible
         assertThat(visibleModel.icon)
             .isEqualTo(
                 Icon.Loaded(
@@ -88,11 +88,11 @@
     @Test
     fun `affordance - wallet not enabled - model is none`() = runBlockingTest {
         setUpState(isWalletEnabled = false)
-        var latest: KeyguardQuickAffordanceConfig.State? = null
+        var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
 
-        val job = underTest.state.onEach { latest = it }.launchIn(this)
+        val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
 
-        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden)
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
 
         job.cancel()
     }
@@ -100,11 +100,11 @@
     @Test
     fun `affordance - query not successful - model is none`() = runBlockingTest {
         setUpState(isWalletQuerySuccessful = false)
-        var latest: KeyguardQuickAffordanceConfig.State? = null
+        var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
 
-        val job = underTest.state.onEach { latest = it }.launchIn(this)
+        val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
 
-        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden)
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
 
         job.cancel()
     }
@@ -112,11 +112,11 @@
     @Test
     fun `affordance - missing icon - model is none`() = runBlockingTest {
         setUpState(hasWalletIcon = false)
-        var latest: KeyguardQuickAffordanceConfig.State? = null
+        var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
 
-        val job = underTest.state.onEach { latest = it }.launchIn(this)
+        val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
 
-        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden)
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
 
         job.cancel()
     }
@@ -124,24 +124,24 @@
     @Test
     fun `affordance - no selected card - model is none`() = runBlockingTest {
         setUpState(hasWalletIcon = false)
-        var latest: KeyguardQuickAffordanceConfig.State? = null
+        var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null
 
-        val job = underTest.state.onEach { latest = it }.launchIn(this)
+        val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this)
 
-        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden)
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
 
         job.cancel()
     }
 
     @Test
-    fun onQuickAffordanceClicked() {
+    fun onQuickAffordanceTriggered() {
         val animationController: ActivityLaunchAnimator.Controller = mock()
         val expandable: Expandable = mock {
             whenever(this.activityLaunchController()).thenReturn(animationController)
         }
 
-        assertThat(underTest.onQuickAffordanceClicked(expandable))
-            .isEqualTo(KeyguardQuickAffordanceConfig.OnClickedResult.Handled)
+        assertThat(underTest.onTriggered(expandable))
+            .isEqualTo(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled)
         verify(walletController)
             .startQuickAccessUiIntent(
                 activityStarter,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index b4d5464..7116cc1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -25,11 +25,12 @@
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
+import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
-import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig
 import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -211,7 +212,11 @@
         MockitoAnnotations.initMocks(this)
         whenever(expandable.activityLaunchController()).thenReturn(animationController)
 
-        homeControls = object : FakeKeyguardQuickAffordanceConfig() {}
+        homeControls =
+            object :
+                FakeKeyguardQuickAffordanceConfig(
+                    BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
+                ) {}
         underTest =
             KeyguardQuickAffordanceInteractor(
                 keyguardInteractor = KeyguardInteractor(repository = FakeKeyguardRepository()),
@@ -224,8 +229,14 @@
                                 ),
                             KeyguardQuickAffordancePosition.BOTTOM_END to
                                 listOf(
-                                    object : FakeKeyguardQuickAffordanceConfig() {},
-                                    object : FakeKeyguardQuickAffordanceConfig() {},
+                                    object :
+                                        FakeKeyguardQuickAffordanceConfig(
+                                            BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
+                                        ) {},
+                                    object :
+                                        FakeKeyguardQuickAffordanceConfig(
+                                            BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER
+                                        ) {},
                                 ),
                         ),
                     ),
@@ -237,30 +248,30 @@
     }
 
     @Test
-    fun onQuickAffordanceClicked() = runBlockingTest {
+    fun onQuickAffordanceTriggered() = runBlockingTest {
         setUpMocks(
             needStrongAuthAfterBoot = needStrongAuthAfterBoot,
             keyguardIsUnlocked = keyguardIsUnlocked,
         )
 
         homeControls.setState(
-            state =
-                KeyguardQuickAffordanceConfig.State.Visible(
+            lockScreenState =
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
                     icon = DRAWABLE,
                 )
         )
-        homeControls.onClickedResult =
+        homeControls.onTriggeredResult =
             if (startActivity) {
-                KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity(
+                KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity(
                     intent = INTENT,
                     canShowWhileLocked = canShowWhileLocked,
                 )
             } else {
-                KeyguardQuickAffordanceConfig.OnClickedResult.Handled
+                KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
             }
 
-        underTest.onQuickAffordanceClicked(
-            configKey = homeControls::class,
+        underTest.onQuickAffordanceTriggered(
+            configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS,
             expandable = expandable,
         )
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
index 65fd6e5..ae32ba6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
@@ -22,13 +22,14 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
+import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
-import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
-import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig
 import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
-import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
+import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -69,9 +70,21 @@
         repository = FakeKeyguardRepository()
         repository.setKeyguardShowing(true)
 
-        homeControls = object : FakeKeyguardQuickAffordanceConfig() {}
-        quickAccessWallet = object : FakeKeyguardQuickAffordanceConfig() {}
-        qrCodeScanner = object : FakeKeyguardQuickAffordanceConfig() {}
+        homeControls =
+            object :
+                FakeKeyguardQuickAffordanceConfig(
+                    BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
+                ) {}
+        quickAccessWallet =
+            object :
+                FakeKeyguardQuickAffordanceConfig(
+                    BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
+                ) {}
+        qrCodeScanner =
+            object :
+                FakeKeyguardQuickAffordanceConfig(
+                    BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER
+                ) {}
 
         underTest =
             KeyguardQuickAffordanceInteractor(
@@ -99,11 +112,11 @@
 
     @Test
     fun `quickAffordance - bottom start affordance is visible`() = runBlockingTest {
-        val configKey = homeControls::class
+        val configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
         homeControls.setState(
-            KeyguardQuickAffordanceConfig.State.Visible(
+            KeyguardQuickAffordanceConfig.LockScreenState.Visible(
                 icon = ICON,
-                toggle = KeyguardQuickAffordanceToggleState.On,
+                activationState = ActivationState.Active,
             )
         )
 
@@ -124,15 +137,15 @@
         assertThat(visibleModel.icon).isEqualTo(ICON)
         assertThat(visibleModel.icon.contentDescription)
             .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
-        assertThat(visibleModel.toggle).isEqualTo(KeyguardQuickAffordanceToggleState.On)
+        assertThat(visibleModel.activationState).isEqualTo(ActivationState.Active)
         job.cancel()
     }
 
     @Test
     fun `quickAffordance - bottom end affordance is visible`() = runBlockingTest {
-        val configKey = quickAccessWallet::class
+        val configKey = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
         quickAccessWallet.setState(
-            KeyguardQuickAffordanceConfig.State.Visible(
+            KeyguardQuickAffordanceConfig.LockScreenState.Visible(
                 icon = ICON,
             )
         )
@@ -154,7 +167,7 @@
         assertThat(visibleModel.icon).isEqualTo(ICON)
         assertThat(visibleModel.icon.contentDescription)
             .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
-        assertThat(visibleModel.toggle).isEqualTo(KeyguardQuickAffordanceToggleState.NotSupported)
+        assertThat(visibleModel.activationState).isEqualTo(ActivationState.NotSupported)
         job.cancel()
     }
 
@@ -162,7 +175,7 @@
     fun `quickAffordance - bottom start affordance hidden while dozing`() = runBlockingTest {
         repository.setDozing(true)
         homeControls.setState(
-            KeyguardQuickAffordanceConfig.State.Visible(
+            KeyguardQuickAffordanceConfig.LockScreenState.Visible(
                 icon = ICON,
             )
         )
@@ -182,7 +195,7 @@
         runBlockingTest {
             repository.setKeyguardShowing(false)
             homeControls.setState(
-                KeyguardQuickAffordanceConfig.State.Visible(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
                     icon = ICON,
                 )
             )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt
index e68c43f..13e2768 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt
@@ -17,8 +17,8 @@
 
 package com.android.systemui.keyguard.domain.quickaffordance
 
-import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
-import kotlin.reflect.KClass
+import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 
 /** Fake implementation of [FakeKeyguardQuickAffordanceRegistry], for tests. */
 class FakeKeyguardQuickAffordanceRegistry(
@@ -33,11 +33,8 @@
     }
 
     override fun get(
-        configClass: KClass<out FakeKeyguardQuickAffordanceConfig>
+        key: String,
     ): FakeKeyguardQuickAffordanceConfig {
-        return configsByPosition.values
-            .flatten()
-            .associateBy { config -> config::class }
-            .getValue(configClass)
+        return configsByPosition.values.flatten().associateBy { config -> config.key }.getValue(key)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index d674c89..f73d1ec 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -23,15 +23,16 @@
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.doze.util.BurnInHelperWrapper
+import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
+import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
-import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
-import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig
 import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
-import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
+import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -40,7 +41,6 @@
 import com.google.common.truth.Truth.assertThat
 import kotlin.math.max
 import kotlin.math.min
-import kotlin.reflect.KClass
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.runBlockingTest
@@ -81,9 +81,21 @@
         whenever(burnInHelperWrapper.burnInOffset(anyInt(), any()))
             .thenReturn(RETURNED_BURN_IN_OFFSET)
 
-        homeControlsQuickAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
-        quickAccessWalletAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
-        qrCodeScannerAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
+        homeControlsQuickAffordanceConfig =
+            object :
+                FakeKeyguardQuickAffordanceConfig(
+                    BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
+                ) {}
+        quickAccessWalletAffordanceConfig =
+            object :
+                FakeKeyguardQuickAffordanceConfig(
+                    BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
+                ) {}
+        qrCodeScannerAffordanceConfig =
+            object :
+                FakeKeyguardQuickAffordanceConfig(
+                    BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER
+                ) {}
         registry =
             FakeKeyguardQuickAffordanceRegistry(
                 mapOf(
@@ -489,42 +501,42 @@
     private suspend fun setUpQuickAffordanceModel(
         position: KeyguardQuickAffordancePosition,
         testConfig: TestConfig,
-    ): KClass<out FakeKeyguardQuickAffordanceConfig> {
+    ): String {
         val config =
             when (position) {
                 KeyguardQuickAffordancePosition.BOTTOM_START -> homeControlsQuickAffordanceConfig
                 KeyguardQuickAffordancePosition.BOTTOM_END -> quickAccessWalletAffordanceConfig
             }
 
-        val state =
+        val lockScreenState =
             if (testConfig.isVisible) {
                 if (testConfig.intent != null) {
-                    config.onClickedResult =
-                        KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity(
+                    config.onTriggeredResult =
+                        KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity(
                             intent = testConfig.intent,
                             canShowWhileLocked = testConfig.canShowWhileLocked,
                         )
                 }
-                KeyguardQuickAffordanceConfig.State.Visible(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(
                     icon = testConfig.icon ?: error("Icon is unexpectedly null!"),
-                    toggle =
+                    activationState =
                         when (testConfig.isActivated) {
-                            true -> KeyguardQuickAffordanceToggleState.On
-                            false -> KeyguardQuickAffordanceToggleState.Off
-                            null -> KeyguardQuickAffordanceToggleState.NotSupported
+                            true -> ActivationState.Active
+                            false -> ActivationState.Inactive
+                            null -> ActivationState.NotSupported
                         }
                 )
             } else {
-                KeyguardQuickAffordanceConfig.State.Hidden
+                KeyguardQuickAffordanceConfig.LockScreenState.Hidden
             }
-        config.setState(state)
-        return config::class
+        config.setState(lockScreenState)
+        return config.key
     }
 
     private fun assertQuickAffordanceViewModel(
         viewModel: KeyguardQuickAffordanceViewModel?,
         testConfig: TestConfig,
-        configKey: KClass<out FakeKeyguardQuickAffordanceConfig>,
+        configKey: String,
     ) {
         checkNotNull(viewModel)
         assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
index 071604d..920801f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
@@ -31,7 +31,7 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.media.dream.MediaDreamComplication
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.shade.testing.FakeNotifPanelEvents
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.phone.KeyguardBypassController
@@ -89,7 +89,7 @@
     private lateinit var mediaHierarchyManager: MediaHierarchyManager
     private lateinit var mediaFrame: ViewGroup
     private val configurationController = FakeConfigurationController()
-    private val notifPanelEvents = FakeNotifPanelEvents()
+    private val notifPanelEvents = ShadeExpansionStateManager()
     private val settings = FakeSettings()
     private lateinit var testableLooper: TestableLooper
     private lateinit var fakeHandler: FakeHandler
@@ -346,7 +346,7 @@
 
     @Test
     fun isCurrentlyInGuidedTransformation_hostsVisible_expandImmediateEnabled_returnsFalse() {
-        notifPanelEvents.changeExpandImmediate(expandImmediate = true)
+        notifPanelEvents.notifyExpandImmediateChange(true)
         goToLockscreen()
         enterGuidedTransformation()
         whenever(lockHost.visible).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
index fdeb3f5..ad19bc2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
@@ -45,6 +45,7 @@
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
+import com.android.systemui.temporarydisplay.chipbar.ChipbarLogger
 import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
@@ -80,6 +81,7 @@
     @Mock private lateinit var configurationController: ConfigurationController
     @Mock private lateinit var falsingManager: FalsingManager
     @Mock private lateinit var falsingCollector: FalsingCollector
+    @Mock private lateinit var chipbarLogger: ChipbarLogger
     @Mock private lateinit var logger: MediaTttLogger
     @Mock private lateinit var mediaTttFlags: MediaTttFlags
     @Mock private lateinit var packageManager: PackageManager
@@ -122,7 +124,7 @@
         chipbarCoordinator =
             FakeChipbarCoordinator(
                 context,
-                logger,
+                chipbarLogger,
                 windowManager,
                 fakeExecutor,
                 accessibilityManager,
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 02f28a2..ac4dd49 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -438,8 +438,6 @@
         when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
 
         mMainHandler = new Handler(Looper.getMainLooper());
-        NotificationPanelViewController.PanelEventsEmitter panelEventsEmitter =
-                new NotificationPanelViewController.PanelEventsEmitter();
 
         mNotificationPanelViewController = new NotificationPanelViewController(
                 mView,
@@ -495,7 +493,6 @@
                 () -> mKeyguardBottomAreaViewController,
                 mKeyguardUnlockAnimationController,
                 mNotificationListContainer,
-                panelEventsEmitter,
                 mNotificationStackSizeCalculator,
                 mUnlockedScreenOffAnimationController,
                 mShadeTransitionController,
@@ -1597,7 +1594,7 @@
         mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
         mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
 
-        verify(mUpdateMonitor).requestFaceAuth(true,
+        verify(mUpdateMonitor).requestFaceAuth(
                 FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED);
     }
 
@@ -1607,7 +1604,7 @@
                 mNotificationPanelViewController.mStatusBarStateListener;
         statusBarStateListener.onStateChanged(KEYGUARD);
         mNotificationPanelViewController.setDozing(false, false);
-        when(mUpdateMonitor.requestFaceAuth(true, NOTIFICATION_PANEL_CLICKED)).thenReturn(false);
+        when(mUpdateMonitor.requestFaceAuth(NOTIFICATION_PANEL_CLICKED)).thenReturn(false);
 
         // This sets the dozing state that is read when onMiddleClicked is eventually invoked.
         mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
@@ -1622,7 +1619,7 @@
                 mNotificationPanelViewController.mStatusBarStateListener;
         statusBarStateListener.onStateChanged(KEYGUARD);
         mNotificationPanelViewController.setDozing(false, false);
-        when(mUpdateMonitor.requestFaceAuth(true, NOTIFICATION_PANEL_CLICKED)).thenReturn(true);
+        when(mUpdateMonitor.requestFaceAuth(NOTIFICATION_PANEL_CLICKED)).thenReturn(true);
 
         // This sets the dozing state that is read when onMiddleClicked is eventually invoked.
         mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
@@ -1642,7 +1639,7 @@
         mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
         mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
 
-        verify(mUpdateMonitor, never()).requestFaceAuth(anyBoolean(), anyString());
+        verify(mUpdateMonitor, never()).requestFaceAuth(anyString());
     }
 
     @Test
@@ -1653,7 +1650,7 @@
 
         mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
 
-        verify(mUpdateMonitor, never()).requestFaceAuth(anyBoolean(), anyString());
+        verify(mUpdateMonitor, never()).requestFaceAuth(anyString());
 
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/testing/FakeNotifPanelEvents.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/testing/FakeNotifPanelEvents.kt
deleted file mode 100644
index d052138..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/testing/FakeNotifPanelEvents.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.shade.testing
-
-import com.android.systemui.shade.NotifPanelEvents
-
-/** Fake implementation of [NotifPanelEvents] for testing. */
-class FakeNotifPanelEvents : NotifPanelEvents {
-
-    private val listeners = mutableListOf<NotifPanelEvents.Listener>()
-
-    override fun registerListener(listener: NotifPanelEvents.Listener) {
-        listeners.add(listener)
-    }
-
-    override fun unregisterListener(listener: NotifPanelEvents.Listener) {
-        listeners.remove(listener)
-    }
-
-    fun changeExpandImmediate(expandImmediate: Boolean) {
-        listeners.forEach { it.onExpandImmediateChanged(expandImmediate) }
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java
index c961cec..b4a5f5c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java
@@ -36,7 +36,8 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.shade.NotifPanelEvents;
+import com.android.systemui.shade.ShadeStateEvents;
+import com.android.systemui.shade.ShadeStateEvents.ShadeStateEventsListener;
 import com.android.systemui.statusbar.notification.collection.GroupEntry;
 import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -71,12 +72,12 @@
     @Mock private StatusBarStateController mStatusBarStateController;
     @Mock private Pluggable.PluggableListener<NotifStabilityManager> mInvalidateListener;
     @Mock private HeadsUpManager mHeadsUpManager;
-    @Mock private NotifPanelEvents mNotifPanelEvents;
+    @Mock private ShadeStateEvents mShadeStateEvents;
     @Mock private VisualStabilityProvider mVisualStabilityProvider;
 
     @Captor private ArgumentCaptor<WakefulnessLifecycle.Observer> mWakefulnessObserverCaptor;
     @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mSBStateListenerCaptor;
-    @Captor private ArgumentCaptor<NotifPanelEvents.Listener> mNotifPanelEventsCallbackCaptor;
+    @Captor private ArgumentCaptor<ShadeStateEventsListener> mNotifPanelEventsCallbackCaptor;
     @Captor private ArgumentCaptor<NotifStabilityManager> mNotifStabilityManagerCaptor;
 
     private FakeSystemClock mFakeSystemClock = new FakeSystemClock();
@@ -84,7 +85,7 @@
 
     private WakefulnessLifecycle.Observer mWakefulnessObserver;
     private StatusBarStateController.StateListener mStatusBarStateListener;
-    private NotifPanelEvents.Listener mNotifPanelEventsCallback;
+    private ShadeStateEvents.ShadeStateEventsListener mNotifPanelEventsCallback;
     private NotifStabilityManager mNotifStabilityManager;
     private NotificationEntry mEntry;
     private GroupEntry mGroupEntry;
@@ -97,7 +98,7 @@
                 mFakeExecutor,
                 mDumpManager,
                 mHeadsUpManager,
-                mNotifPanelEvents,
+                mShadeStateEvents,
                 mStatusBarStateController,
                 mVisualStabilityProvider,
                 mWakefulnessLifecycle);
@@ -111,7 +112,8 @@
         verify(mStatusBarStateController).addCallback(mSBStateListenerCaptor.capture());
         mStatusBarStateListener = mSBStateListenerCaptor.getValue();
 
-        verify(mNotifPanelEvents).registerListener(mNotifPanelEventsCallbackCaptor.capture());
+        verify(mShadeStateEvents).addShadeStateEventsListener(
+                mNotifPanelEventsCallbackCaptor.capture());
         mNotifPanelEventsCallback = mNotifPanelEventsCallbackCaptor.getValue();
 
         verify(mNotifPipeline).setVisualStabilityManager(mNotifStabilityManagerCaptor.capture());
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 91aecd8..dceb4ff 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
@@ -78,6 +78,7 @@
 
 import org.junit.Assert;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -89,6 +90,7 @@
 /**
  * Tests for {@link NotificationStackScrollLayout}.
  */
+@Ignore("b/255552856")
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
index 6ff7b7c..de1fec8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -24,7 +24,14 @@
     private val _subscriptionsModelFlow = MutableStateFlow(MobileSubscriptionModel())
     override val subscriptionModelFlow: Flow<MobileSubscriptionModel> = _subscriptionsModelFlow
 
+    private val _dataEnabled = MutableStateFlow(true)
+    override val dataEnabled = _dataEnabled
+
     fun setMobileSubscriptionModel(model: MobileSubscriptionModel) {
         _subscriptionsModelFlow.value = model
     }
+
+    fun setDataEnabled(enabled: Boolean) {
+        _dataEnabled.value = enabled
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
index c88d468..813e750 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -50,7 +50,7 @@
         _activeMobileDataSubscriptionId.value = subId
     }
 
-    fun setMobileConnectionRepositoryForId(subId: Int, repo: MobileConnectionRepository) {
-        subIdRepos[subId] = repo
+    fun setMobileConnectionRepositoryMap(connections: Map<Int, MobileConnectionRepository>) {
+        connections.forEach { entry -> subIdRepos[entry.key] = entry.value }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
index 775e6db..0939364 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
@@ -20,16 +20,20 @@
 import android.telephony.ServiceState
 import android.telephony.SignalStrength
 import android.telephony.SubscriptionInfo
-import android.telephony.SubscriptionManager
 import android.telephony.TelephonyCallback
 import android.telephony.TelephonyCallback.ServiceStateListener
 import android.telephony.TelephonyDisplayInfo
 import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_CA
 import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.DATA_CONNECTED
+import android.telephony.TelephonyManager.DATA_CONNECTING
+import android.telephony.TelephonyManager.DATA_DISCONNECTED
+import android.telephony.TelephonyManager.DATA_DISCONNECTING
 import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
 import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
@@ -59,7 +63,6 @@
 class MobileConnectionRepositoryTest : SysuiTestCase() {
     private lateinit var underTest: MobileConnectionRepositoryImpl
 
-    @Mock private lateinit var subscriptionManager: SubscriptionManager
     @Mock private lateinit var telephonyManager: TelephonyManager
     @Mock private lateinit var logger: ConnectivityPipelineLogger
 
@@ -148,16 +151,61 @@
         }
 
     @Test
-    fun testFlowForSubId_dataConnectionState() =
+    fun testFlowForSubId_dataConnectionState_connected() =
         runBlocking(IMMEDIATE) {
             var latest: MobileSubscriptionModel? = null
             val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
 
             val callback =
                 getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
-            callback.onDataConnectionStateChanged(100, 200 /* unused */)
+            callback.onDataConnectionStateChanged(DATA_CONNECTED, 200 /* unused */)
 
-            assertThat(latest?.dataConnectionState).isEqualTo(100)
+            assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Connected)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_dataConnectionState_connecting() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback =
+                getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+            callback.onDataConnectionStateChanged(DATA_CONNECTING, 200 /* unused */)
+
+            assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Connecting)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_dataConnectionState_disconnected() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback =
+                getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+            callback.onDataConnectionStateChanged(DATA_DISCONNECTED, 200 /* unused */)
+
+            assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Disconnected)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_dataConnectionState_disconnecting() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback =
+                getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+            callback.onDataConnectionStateChanged(DATA_DISCONNECTING, 200 /* unused */)
+
+            assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Disconnecting)
 
             job.cancel()
         }
@@ -241,6 +289,32 @@
             job.cancel()
         }
 
+    @Test
+    fun dataEnabled_isEnabled() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true)
+
+            var latest: Boolean? = null
+            val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun dataEnabled_isDisabled() =
+        runBlocking(IMMEDIATE) {
+            whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
+
+            var latest: Boolean? = null
+            val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
     private fun getTelephonyCallbacks(): List<TelephonyCallback> {
         val callbackCaptor = argumentCaptor<TelephonyCallback>()
         Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
index cd4dbeb..5611c44 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
@@ -22,19 +22,22 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 
 class FakeMobileIconInteractor : MobileIconInteractor {
-    private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.UNKNOWN)
+    private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.THREE_G)
     override val networkTypeIconGroup = _iconGroup
 
-    private val _isEmergencyOnly = MutableStateFlow<Boolean>(false)
+    private val _isEmergencyOnly = MutableStateFlow(false)
     override val isEmergencyOnly = _isEmergencyOnly
 
-    private val _level = MutableStateFlow<Int>(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
+    private val _isDataEnabled = MutableStateFlow(true)
+    override val isDataEnabled = _isDataEnabled
+
+    private val _level = MutableStateFlow(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
     override val level = _level
 
-    private val _numberOfLevels = MutableStateFlow<Int>(4)
+    private val _numberOfLevels = MutableStateFlow(4)
     override val numberOfLevels = _numberOfLevels
 
-    private val _cutOut = MutableStateFlow<Boolean>(false)
+    private val _cutOut = MutableStateFlow(false)
     override val cutOut = _cutOut
 
     fun setIconGroup(group: SignalIcon.MobileIconGroup) {
@@ -45,6 +48,10 @@
         _isEmergencyOnly.value = emergency
     }
 
+    fun setIsDataEnabled(enabled: Boolean) {
+        _isDataEnabled.value = enabled
+    }
+
     fun setLevel(level: Int) {
         _level.value = level
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index b01efd1..877ce0e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -19,6 +19,7 @@
 import android.telephony.SubscriptionInfo
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
@@ -41,7 +42,7 @@
 class MobileIconsInteractorTest : SysuiTestCase() {
     private lateinit var underTest: MobileIconsInteractor
     private val userSetupRepository = FakeUserSetupRepository()
-    private val subscriptionsRepository = FakeMobileConnectionsRepository()
+    private val connectionsRepository = FakeMobileConnectionsRepository()
     private val mobileMappingsProxy = FakeMobileMappingsProxy()
     private val scope = CoroutineScope(IMMEDIATE)
 
@@ -50,9 +51,20 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+
+        connectionsRepository.setMobileConnectionRepositoryMap(
+            mapOf(
+                SUB_1_ID to CONNECTION_1,
+                SUB_2_ID to CONNECTION_2,
+                SUB_3_ID to CONNECTION_3,
+                SUB_4_ID to CONNECTION_4,
+            )
+        )
+        connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID)
+
         underTest =
             MobileIconsInteractorImpl(
-                subscriptionsRepository,
+                connectionsRepository,
                 carrierConfigTracker,
                 mobileMappingsProxy,
                 userSetupRepository,
@@ -76,7 +88,7 @@
     @Test
     fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() =
         runBlocking(IMMEDIATE) {
-            subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
+            connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
 
             var latest: List<SubscriptionInfo>? = null
             val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
@@ -89,8 +101,8 @@
     @Test
     fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_3() =
         runBlocking(IMMEDIATE) {
-            subscriptionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
-            subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
+            connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
+            connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
             whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
                 .thenReturn(false)
 
@@ -106,8 +118,8 @@
     @Test
     fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_4() =
         runBlocking(IMMEDIATE) {
-            subscriptionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
-            subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID)
+            connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
+            connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID)
             whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
                 .thenReturn(false)
 
@@ -123,8 +135,8 @@
     @Test
     fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_active_1() =
         runBlocking(IMMEDIATE) {
-            subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP))
-            subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID)
+            connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP))
+            connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID)
             whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
                 .thenReturn(true)
 
@@ -141,8 +153,8 @@
     @Test
     fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_nonActive_1() =
         runBlocking(IMMEDIATE) {
-            subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP))
-            subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
+            connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP))
+            connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
             whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
                 .thenReturn(true)
 
@@ -162,10 +174,12 @@
         private const val SUB_1_ID = 1
         private val SUB_1 =
             mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+        private val CONNECTION_1 = FakeMobileConnectionRepository()
 
         private const val SUB_2_ID = 2
         private val SUB_2 =
             mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
+        private val CONNECTION_2 = FakeMobileConnectionRepository()
 
         private const val SUB_3_ID = 3
         private val SUB_3_OPP =
@@ -173,6 +187,7 @@
                 whenever(it.subscriptionId).thenReturn(SUB_3_ID)
                 whenever(it.isOpportunistic).thenReturn(true)
             }
+        private val CONNECTION_3 = FakeMobileConnectionRepository()
 
         private const val SUB_4_ID = 4
         private val SUB_4_OPP =
@@ -180,5 +195,6 @@
                 whenever(it.subscriptionId).thenReturn(SUB_4_ID)
                 whenever(it.isOpportunistic).thenReturn(true)
             }
+        private val CONNECTION_4 = FakeMobileConnectionRepository()
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index b374abb..ce0f33f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -18,8 +18,10 @@
 
 import androidx.test.filters.SmallTest
 import com.android.settingslib.graph.SignalDrawable
-import com.android.settingslib.mobile.TelephonyIcons
+import com.android.settingslib.mobile.TelephonyIcons.THREE_G
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.google.common.truth.Truth.assertThat
@@ -27,6 +29,7 @@
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mock
@@ -44,7 +47,7 @@
         interactor.apply {
             setLevel(1)
             setCutOut(false)
-            setIconGroup(TelephonyIcons.THREE_G)
+            setIconGroup(THREE_G)
             setIsEmergencyOnly(false)
             setNumberOfLevels(4)
         }
@@ -62,6 +65,60 @@
             job.cancel()
         }
 
+    @Test
+    fun networkType_dataEnabled_groupIsRepresented() =
+        runBlocking(IMMEDIATE) {
+            val expected =
+                Icon.Resource(
+                    THREE_G.dataType,
+                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                )
+            interactor.setIconGroup(THREE_G)
+
+            var latest: Icon? = null
+            val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(expected)
+
+            job.cancel()
+        }
+
+    @Test
+    fun networkType_nullWhenDisabled() =
+        runBlocking(IMMEDIATE) {
+            interactor.setIconGroup(THREE_G)
+            interactor.setIsDataEnabled(false)
+            var latest: Icon? = null
+            val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isNull()
+
+            job.cancel()
+        }
+
+    @Test
+    fun networkType_null_changeToDisabled() =
+        runBlocking(IMMEDIATE) {
+            val expected =
+                Icon.Resource(
+                    THREE_G.dataType,
+                    ContentDescription.Resource(THREE_G.dataContentDescription)
+                )
+            interactor.setIconGroup(THREE_G)
+            interactor.setIsDataEnabled(true)
+            var latest: Icon? = null
+            val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(expected)
+
+            interactor.setIsDataEnabled(false)
+            yield()
+
+            assertThat(latest).isNull()
+
+            job.cancel()
+        }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
         private const val SUB_1_ID = 1
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
index b68eb88..91b5c35 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
@@ -41,6 +41,7 @@
 import org.mockito.Mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
@@ -85,10 +86,29 @@
     }
 
     @Test
-    fun displayView_viewAdded() {
-        underTest.displayView(getState())
+    fun displayView_viewAddedWithCorrectTitle() {
+        underTest.displayView(
+            ViewInfo(
+                name = "name",
+                windowTitle = "Fake Window Title",
+            )
+        )
 
-        verify(windowManager).addView(any(), any())
+        val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+        verify(windowManager).addView(any(), capture(windowParamsCaptor))
+        assertThat(windowParamsCaptor.value!!.title).isEqualTo("Fake Window Title")
+    }
+
+    @Test
+    fun displayView_logged() {
+        underTest.displayView(
+            ViewInfo(
+                name = "name",
+                windowTitle = "Fake Window Title",
+            )
+        )
+
+        verify(logger).logViewAddition("Fake Window Title")
     }
 
     @Test
@@ -110,7 +130,7 @@
     }
 
     @Test
-    fun displayView_twice_viewNotAddedTwice() {
+    fun displayView_twiceWithSameWindowTitle_viewNotAddedTwice() {
         underTest.displayView(getState())
         reset(windowManager)
 
@@ -119,6 +139,32 @@
     }
 
     @Test
+    fun displayView_twiceWithDifferentWindowTitles_oldViewRemovedNewViewAdded() {
+        underTest.displayView(
+            ViewInfo(
+                name = "name",
+                windowTitle = "First Fake Window Title",
+            )
+        )
+
+        underTest.displayView(
+            ViewInfo(
+                name = "name",
+                windowTitle = "Second Fake Window Title",
+            )
+        )
+
+        val viewCaptor = argumentCaptor<View>()
+        val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>()
+
+        verify(windowManager, times(2)).addView(capture(viewCaptor), capture(windowParamsCaptor))
+
+        assertThat(windowParamsCaptor.allValues[0].title).isEqualTo("First Fake Window Title")
+        assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title")
+        verify(windowManager).removeView(viewCaptor.allValues[0])
+    }
+
+    @Test
     fun displayView_viewDoesNotDisappearsBeforeTimeout() {
         val state = getState()
         underTest.displayView(state)
@@ -197,7 +243,7 @@
         underTest.removeView(reason)
 
         verify(windowManager).removeView(any())
-        verify(logger).logChipRemoval(reason)
+        verify(logger).logViewRemoval(reason)
     }
 
     @Test
@@ -232,8 +278,6 @@
         configurationController,
         powerManager,
         R.layout.chipbar,
-        "Window Title",
-        "WAKE_REASON",
     ) {
         var mostRecentViewInfo: ViewInfo? = null
 
@@ -250,9 +294,12 @@
         }
     }
 
-    inner class ViewInfo(val name: String) : TemporaryViewInfo {
-        override fun getTimeoutMs() = 1L
-    }
+    inner class ViewInfo(
+        val name: String,
+        override val windowTitle: String = "Window Title",
+        override val wakeReason: String = "WAKE_REASON",
+        override val timeoutMs: Int = 1
+    ) : TemporaryViewInfo()
 }
 
 private const val TIMEOUT_MS = 10000L
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 13e9f60..d155050 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt
@@ -43,20 +43,21 @@
     }
 
     @Test
-    fun logChipAddition_bufferHasLog() {
-        logger.logChipAddition()
+    fun logViewAddition_bufferHasLog() {
+        logger.logViewAddition("Test Window Title")
 
         val stringWriter = StringWriter()
         buffer.dump(PrintWriter(stringWriter), tailLength = 0)
         val actualString = stringWriter.toString()
 
         assertThat(actualString).contains(TAG)
+        assertThat(actualString).contains("Test Window Title")
     }
 
     @Test
-    fun logChipRemoval_bufferHasTagAndReason() {
+    fun logViewRemoval_bufferHasTagAndReason() {
         val reason = "test reason"
-        logger.logChipRemoval(reason)
+        logger.logViewRemoval(reason)
 
         val stringWriter = StringWriter()
         buffer.dump(PrintWriter(stringWriter), tailLength = 0)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
index 9fbf159..f643973 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
@@ -35,12 +35,12 @@
 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.systemui.util.view.ViewUtil
 import com.google.common.truth.Truth.assertThat
@@ -60,7 +60,7 @@
 class ChipbarCoordinatorTest : SysuiTestCase() {
     private lateinit var underTest: FakeChipbarCoordinator
 
-    @Mock private lateinit var logger: MediaTttLogger
+    @Mock private lateinit var logger: ChipbarLogger
     @Mock private lateinit var accessibilityManager: AccessibilityManager
     @Mock private lateinit var configurationController: ConfigurationController
     @Mock private lateinit var powerManager: PowerManager
@@ -105,7 +105,7 @@
         val drawable = context.getDrawable(R.drawable.ic_celebration)!!
 
         underTest.displayView(
-            ChipbarInfo(
+            createChipbarInfo(
                 Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")),
                 Text.Loaded("text"),
                 endItem = null,
@@ -121,7 +121,7 @@
     fun displayView_resourceIcon_correctlyRendered() {
         val contentDescription = ContentDescription.Resource(R.string.controls_error_timeout)
         underTest.displayView(
-            ChipbarInfo(
+            createChipbarInfo(
                 Icon.Resource(R.drawable.ic_cake, contentDescription),
                 Text.Loaded("text"),
                 endItem = null,
@@ -136,7 +136,7 @@
     @Test
     fun displayView_loadedText_correctlyRendered() {
         underTest.displayView(
-            ChipbarInfo(
+            createChipbarInfo(
                 Icon.Resource(R.id.check_box, null),
                 Text.Loaded("display view text here"),
                 endItem = null,
@@ -149,7 +149,7 @@
     @Test
     fun displayView_resourceText_correctlyRendered() {
         underTest.displayView(
-            ChipbarInfo(
+            createChipbarInfo(
                 Icon.Resource(R.id.check_box, null),
                 Text.Resource(R.string.screenrecord_start_error),
                 endItem = null,
@@ -163,7 +163,7 @@
     @Test
     fun displayView_endItemNull_correctlyRendered() {
         underTest.displayView(
-            ChipbarInfo(
+            createChipbarInfo(
                 Icon.Resource(R.id.check_box, null),
                 Text.Loaded("text"),
                 endItem = null,
@@ -179,7 +179,7 @@
     @Test
     fun displayView_endItemLoading_correctlyRendered() {
         underTest.displayView(
-            ChipbarInfo(
+            createChipbarInfo(
                 Icon.Resource(R.id.check_box, null),
                 Text.Loaded("text"),
                 endItem = ChipbarEndItem.Loading,
@@ -195,7 +195,7 @@
     @Test
     fun displayView_endItemError_correctlyRendered() {
         underTest.displayView(
-            ChipbarInfo(
+            createChipbarInfo(
                 Icon.Resource(R.id.check_box, null),
                 Text.Loaded("text"),
                 endItem = ChipbarEndItem.Error,
@@ -211,7 +211,7 @@
     @Test
     fun displayView_endItemButton_correctlyRendered() {
         underTest.displayView(
-            ChipbarInfo(
+            createChipbarInfo(
                 Icon.Resource(R.id.check_box, null),
                 Text.Loaded("text"),
                 endItem =
@@ -237,7 +237,7 @@
         val buttonClickListener = View.OnClickListener { isClicked = true }
 
         underTest.displayView(
-            ChipbarInfo(
+            createChipbarInfo(
                 Icon.Resource(R.id.check_box, null),
                 Text.Loaded("text"),
                 endItem =
@@ -260,7 +260,7 @@
         val buttonClickListener = View.OnClickListener { isClicked = true }
 
         underTest.displayView(
-            ChipbarInfo(
+            createChipbarInfo(
                 Icon.Resource(R.id.check_box, null),
                 Text.Loaded("text"),
                 endItem =
@@ -279,7 +279,7 @@
     @Test
     fun displayView_vibrationEffect_doubleClickEffect() {
         underTest.displayView(
-            ChipbarInfo(
+            createChipbarInfo(
                 Icon.Resource(R.id.check_box, null),
                 Text.Loaded("text"),
                 endItem = null,
@@ -296,7 +296,7 @@
         val drawable = context.getDrawable(R.drawable.ic_celebration)!!
 
         underTest.displayView(
-            ChipbarInfo(
+            createChipbarInfo(
                 Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")),
                 Text.Loaded("title text"),
                 endItem = ChipbarEndItem.Loading,
@@ -314,7 +314,7 @@
         // WHEN the view is updated
         val newDrawable = context.getDrawable(R.drawable.ic_cake)!!
         underTest.updateView(
-            ChipbarInfo(
+            createChipbarInfo(
                 Icon.Loaded(newDrawable, ContentDescription.Loaded("new CD")),
                 Text.Loaded("new title text"),
                 endItem = ChipbarEndItem.Error,
@@ -331,6 +331,47 @@
         assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
     }
 
+    @Test
+    fun viewUpdates_logged() {
+        val drawable = context.getDrawable(R.drawable.ic_celebration)!!
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")),
+                Text.Loaded("title text"),
+                endItem = ChipbarEndItem.Loading,
+            )
+        )
+
+        verify(logger).logViewUpdate(eq(WINDOW_TITLE), eq("title text"), any())
+
+        underTest.displayView(
+            createChipbarInfo(
+                Icon.Loaded(drawable, ContentDescription.Loaded("new CD")),
+                Text.Loaded("new title text"),
+                endItem = ChipbarEndItem.Error,
+            )
+        )
+
+        verify(logger).logViewUpdate(eq(WINDOW_TITLE), eq("new title text"), any())
+    }
+
+    private fun createChipbarInfo(
+        startIcon: Icon,
+        text: Text,
+        endItem: ChipbarEndItem?,
+        vibrationEffect: VibrationEffect? = null,
+    ): ChipbarInfo {
+        return ChipbarInfo(
+            startIcon,
+            text,
+            endItem,
+            vibrationEffect,
+            windowTitle = WINDOW_TITLE,
+            wakeReason = WAKE_REASON,
+            timeoutMs = TIMEOUT,
+        )
+    }
+
     private fun ViewGroup.getStartIconView() = this.requireViewById<ImageView>(R.id.start_icon)
 
     private fun ViewGroup.getChipText(): String =
@@ -350,3 +391,5 @@
 }
 
 private const val TIMEOUT = 10000
+private const val WINDOW_TITLE = "Test Chipbar Window Title"
+private const val WAKE_REASON = "TEST_CHIPBAR_WAKE_REASON"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
index 17d4023..574f70e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
@@ -22,8 +22,6 @@
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
 import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.media.taptotransfer.common.MediaTttLogger
-import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -33,7 +31,7 @@
 /** A fake implementation of [ChipbarCoordinator] for testing. */
 class FakeChipbarCoordinator(
     context: Context,
-    @MediaTttReceiverLogger logger: MediaTttLogger,
+    logger: ChipbarLogger,
     windowManager: WindowManager,
     mainExecutor: DelayableExecutor,
     accessibilityManager: AccessibilityManager,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt
new file mode 100644
index 0000000..96658c6
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt
@@ -0,0 +1,48 @@
+package com.android.systemui.biometrics.data.repository
+
+import android.hardware.biometrics.PromptInfo
+import com.android.systemui.biometrics.data.model.PromptKind
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Fake implementation of [PromptRepository] for tests. */
+class FakePromptRepository : PromptRepository {
+
+    private val _isShowing = MutableStateFlow(false)
+    override val isShowing = _isShowing.asStateFlow()
+
+    private val _promptInfo = MutableStateFlow<PromptInfo?>(null)
+    override val promptInfo = _promptInfo.asStateFlow()
+
+    private val _userId = MutableStateFlow<Int?>(null)
+    override val userId = _userId.asStateFlow()
+
+    private var _challenge = MutableStateFlow<Long?>(null)
+    override val challenge = _challenge.asStateFlow()
+
+    private val _kind = MutableStateFlow(PromptKind.ANY_BIOMETRIC)
+    override val kind = _kind.asStateFlow()
+
+    override fun setPrompt(
+        promptInfo: PromptInfo,
+        userId: Int,
+        gatekeeperChallenge: Long?,
+        kind: PromptKind
+    ) {
+        _promptInfo.value = promptInfo
+        _userId.value = userId
+        _challenge.value = gatekeeperChallenge
+        _kind.value = kind
+    }
+
+    override fun unsetPrompt() {
+        _promptInfo.value = null
+        _userId.value = null
+        _challenge.value = null
+        _kind.value = PromptKind.ANY_BIOMETRIC
+    }
+
+    fun setIsShowing(showing: Boolean) {
+        _isShowing.value = showing
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FakeCredentialInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FakeCredentialInteractor.kt
new file mode 100644
index 0000000..fbe291e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FakeCredentialInteractor.kt
@@ -0,0 +1,31 @@
+package com.android.systemui.biometrics.domain.interactor
+
+import com.android.internal.widget.LockscreenCredential
+import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+/** Fake implementation of [CredentialInteractor] for tests. */
+class FakeCredentialInteractor : CredentialInteractor {
+
+    /** Sets return value for [isStealthModeActive]. */
+    var stealthMode: Boolean = false
+
+    /** Sets return value for [getCredentialOwnerOrSelfId]. */
+    var credentialOwnerId: Int? = null
+
+    override fun isStealthModeActive(userId: Int): Boolean = stealthMode
+
+    override fun getCredentialOwnerOrSelfId(userId: Int): Int = credentialOwnerId ?: userId
+
+    override fun verifyCredential(
+        request: BiometricPromptRequest.Credential,
+        credential: LockscreenCredential,
+    ): Flow<CredentialStatus> = verifyCredentialResponse(credential)
+
+    /** Sets the result value for [verifyCredential]. */
+    var verifyCredentialResponse: (credential: LockscreenCredential) -> Flow<CredentialStatus> =
+        { _ ->
+            flowOf(CredentialStatus.Fail.Error("invalid"))
+        }
+}
diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
index fc628cf..000bafe 100644
--- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
+++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
@@ -44,6 +44,7 @@
 import android.window.DisplayWindowPolicyController;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.BlockedAppStreamingActivity;
 
 import java.util.List;
@@ -112,7 +113,9 @@
     final ArraySet<Integer> mRunningUids = new ArraySet<>();
     @Nullable private final ActivityListener mActivityListener;
     private final Handler mHandler = new Handler(Looper.getMainLooper());
-    private final ArraySet<RunningAppsChangedListener> mRunningAppsChangedListener =
+    @NonNull
+    @GuardedBy("mGenericWindowPolicyControllerLock")
+    private final ArraySet<RunningAppsChangedListener> mRunningAppsChangedListeners =
             new ArraySet<>();
     @Nullable
     private final @AssociationRequest.DeviceProfile String mDeviceProfile;
@@ -178,12 +181,16 @@
 
     /** Register a listener for running applications changes. */
     public void registerRunningAppsChangedListener(@NonNull RunningAppsChangedListener listener) {
-        mRunningAppsChangedListener.add(listener);
+        synchronized (mGenericWindowPolicyControllerLock) {
+            mRunningAppsChangedListeners.add(listener);
+        }
     }
 
     /** Unregister a listener for running applications changes. */
     public void unregisterRunningAppsChangedListener(@NonNull RunningAppsChangedListener listener) {
-        mRunningAppsChangedListener.remove(listener);
+        synchronized (mGenericWindowPolicyControllerLock) {
+            mRunningAppsChangedListeners.remove(listener);
+        }
     }
 
     @Override
@@ -283,12 +290,16 @@
                 // Post callback on the main thread so it doesn't block activity launching
                 mHandler.post(() -> mActivityListener.onDisplayEmpty(mDisplayId));
             }
-        }
-        mHandler.post(() -> {
-            for (RunningAppsChangedListener listener : mRunningAppsChangedListener) {
-                listener.onRunningAppsChanged(runningUids);
+            if (!mRunningAppsChangedListeners.isEmpty()) {
+                final ArraySet<RunningAppsChangedListener> listeners =
+                        new ArraySet<>(mRunningAppsChangedListeners);
+                mHandler.post(() -> {
+                    for (RunningAppsChangedListener listener : listeners) {
+                        listener.onRunningAppsChanged(runningUids);
+                    }
+                });
             }
-        });
+        }
     }
 
     @Override
@@ -354,4 +365,11 @@
         }
         return true;
     }
+
+    @VisibleForTesting
+    int getRunningAppsChangedListenersSizeForTesting() {
+        synchronized (mGenericWindowPolicyControllerLock) {
+            return mRunningAppsChangedListeners.size();
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index c54d7d1..3032f17 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -3891,24 +3891,29 @@
                             finishForceStopPackageLocked(packageName, appInfo.uid);
                         }
                     }
-                    final Intent intent = new Intent(Intent.ACTION_PACKAGE_DATA_CLEARED,
-                            Uri.fromParts("package", packageName, null));
-                    intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
-                            | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
-                    intent.putExtra(Intent.EXTRA_UID, (appInfo != null) ? appInfo.uid : -1);
-                    intent.putExtra(Intent.EXTRA_USER_HANDLE, resolvedUserId);
-                    final int[] visibilityAllowList =
-                            mPackageManagerInt.getVisibilityAllowList(packageName, resolvedUserId);
-                    if (isInstantApp) {
-                        intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
-                        broadcastIntentInPackage("android", null, SYSTEM_UID, uid, pid, intent,
-                                null, null, null, 0, null, null, permission.ACCESS_INSTANT_APPS,
-                                null, false, false, resolvedUserId, false, null,
-                                visibilityAllowList);
-                    } else {
-                        broadcastIntentInPackage("android", null, SYSTEM_UID, uid, pid, intent,
-                                null, null, null, 0, null, null, null, null, false, false,
-                                resolvedUserId, false, null, visibilityAllowList);
+
+                    if (succeeded) {
+                        final Intent intent = new Intent(Intent.ACTION_PACKAGE_DATA_CLEARED,
+                                Uri.fromParts("package", packageName, null /* fragment */));
+                        intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
+                                | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+                        intent.putExtra(Intent.EXTRA_UID,
+                                (appInfo != null) ? appInfo.uid : INVALID_UID);
+                        intent.putExtra(Intent.EXTRA_USER_HANDLE, resolvedUserId);
+                        if (isInstantApp) {
+                            intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
+                        }
+                        final int[] visibilityAllowList = mPackageManagerInt.getVisibilityAllowList(
+                                packageName, resolvedUserId);
+
+                        broadcastIntentInPackage("android", null /* featureId */,
+                                SYSTEM_UID, uid, pid, intent, null /* resolvedType */,
+                                null /* resultToApp */, null /* resultTo */, 0 /* resultCode */,
+                                null /* resultData */, null /* resultExtras */,
+                                isInstantApp ? permission.ACCESS_INSTANT_APPS : null,
+                                null /* bOptions */, false /* serialized */, false /* sticky */,
+                                resolvedUserId, false /* allowBackgroundActivityStarts */,
+                                null /* backgroundActivityStartsToken */, visibilityAllowList);
                     }
 
                     if (observer != null) {
@@ -13860,6 +13865,9 @@
             @Nullable IBinder backgroundActivityStartsToken,
             @Nullable int[] broadcastAllowList,
             @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver) {
+        // Ensure all internal loopers are registered for idle checks
+        BroadcastLoopers.addMyLooper();
+
         if ((resultTo != null) && (resultToApp == null)) {
             if (resultTo.asBinder() instanceof BinderProxy) {
                 // Warn when requesting results without a way to deliver them
@@ -18110,6 +18118,7 @@
 
     public void waitForBroadcastIdle(@Nullable PrintWriter pw) {
         enforceCallingPermission(permission.DUMP, "waitForBroadcastIdle()");
+        BroadcastLoopers.waitForIdle(pw);
         for (BroadcastQueue queue : mBroadcastQueues) {
             queue.waitForIdle(pw);
         }
@@ -18121,6 +18130,7 @@
 
     public void waitForBroadcastBarrier(@Nullable PrintWriter pw) {
         enforceCallingPermission(permission.DUMP, "waitForBroadcastBarrier()");
+        BroadcastLoopers.waitForIdle(pw);
         for (BroadcastQueue queue : mBroadcastQueues) {
             queue.waitForBarrier(pw);
         }
diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java
index 4590c85..417a0e5 100644
--- a/services/core/java/com/android/server/am/BroadcastConstants.java
+++ b/services/core/java/com/android/server/am/BroadcastConstants.java
@@ -133,7 +133,7 @@
      */
     public boolean MODERN_QUEUE_ENABLED = DEFAULT_MODERN_QUEUE_ENABLED;
     private static final String KEY_MODERN_QUEUE_ENABLED = "modern_queue_enabled";
-    private static final boolean DEFAULT_MODERN_QUEUE_ENABLED = false;
+    private static final boolean DEFAULT_MODERN_QUEUE_ENABLED = true;
 
     /**
      * For {@link BroadcastQueueModernImpl}: Maximum number of process queues to
diff --git a/services/core/java/com/android/server/am/BroadcastLoopers.java b/services/core/java/com/android/server/am/BroadcastLoopers.java
new file mode 100644
index 0000000..bebb484
--- /dev/null
+++ b/services/core/java/com/android/server/am/BroadcastLoopers.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.am;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.SystemClock;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Collection of {@link Looper} that are known to be used for broadcast dispatch
+ * within the system. This collection can be useful for callers interested in
+ * confirming that all pending broadcasts have been successfully enqueued.
+ */
+public class BroadcastLoopers {
+    private static final String TAG = "BroadcastLoopers";
+
+    private static final ArraySet<Looper> sLoopers = new ArraySet<>();
+
+    /**
+     * Register the given {@link Looper} as possibly having messages that will
+     * dispatch broadcasts.
+     */
+    public static void addLooper(@NonNull Looper looper) {
+        synchronized (sLoopers) {
+            sLoopers.add(Objects.requireNonNull(looper));
+        }
+    }
+
+    /**
+     * If the current thread is hosting a {@link Looper}, then register it as
+     * possibly having messages that will dispatch broadcasts.
+     */
+    public static void addMyLooper() {
+        final Looper looper = Looper.myLooper();
+        if (looper != null) {
+            synchronized (sLoopers) {
+                if (sLoopers.add(looper)) {
+                    Slog.w(TAG, "Found previously unknown looper " + looper.getThread());
+                }
+            }
+        }
+    }
+
+    /**
+     * Wait for all registered {@link Looper} instances to become idle, as
+     * defined by {@link MessageQueue#isIdle()}. Note that {@link Message#when}
+     * still in the future are ignored for the purposes of the idle test.
+     */
+    public static void waitForIdle(@Nullable PrintWriter pw) {
+        final CountDownLatch latch;
+        synchronized (sLoopers) {
+            final int N = sLoopers.size();
+            latch = new CountDownLatch(N);
+            for (int i = 0; i < N; i++) {
+                final MessageQueue queue = sLoopers.valueAt(i).getQueue();
+                if (queue.isIdle()) {
+                    latch.countDown();
+                } else {
+                    queue.addIdleHandler(() -> {
+                        latch.countDown();
+                        return false;
+                    });
+                }
+            }
+        }
+
+        long lastPrint = 0;
+        while (latch.getCount() > 0) {
+            final long now = SystemClock.uptimeMillis();
+            if (now >= lastPrint + 1000) {
+                lastPrint = now;
+                logv("Waiting for " + latch.getCount() + " loopers to drain...", pw);
+            }
+            SystemClock.sleep(100);
+        }
+        logv("Loopers drained!", pw);
+    }
+
+    private static void logv(@NonNull String msg, @Nullable PrintWriter pw) {
+        Slog.v(TAG, msg);
+        if (pw != null) {
+            pw.println(msg);
+            pw.flush();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index 9ec1fb4..f7d24e9 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -831,7 +831,7 @@
 
     @NeverCompile
     public void dumpLocked(@UptimeMillisLong long now, @NonNull IndentingPrintWriter pw) {
-        if ((mActive == null) && mPending.isEmpty()) return;
+        if ((mActive == null) && isEmpty()) return;
 
         pw.print(toShortString());
         if (isRunnable()) {
@@ -847,6 +847,10 @@
         if (mActive != null) {
             dumpRecord(now, pw, mActive, mActiveIndex, mActiveBlockedUntilTerminalCount);
         }
+        for (SomeArgs args : mPendingUrgent) {
+            final BroadcastRecord r = (BroadcastRecord) args.arg1;
+            dumpRecord(now, pw, r, args.argi1, args.argi2);
+        }
         for (SomeArgs args : mPending) {
             final BroadcastRecord r = (BroadcastRecord) args.arg1;
             dumpRecord(now, pw, r, args.argi1, args.argi2);
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index ab19ae3..9e9eb71 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -365,6 +365,13 @@
             BroadcastProcessQueue nextQueue = queue.runnableAtNext;
             final long runnableAt = queue.getRunnableAt();
 
+            // When broadcasts are skipped or failed during list traversal, we
+            // might encounter a queue that is no longer runnable; skip it
+            if (!queue.isRunnable()) {
+                queue = nextQueue;
+                continue;
+            }
+
             // If queues beyond this point aren't ready to run yet, schedule
             // another pass when they'll be runnable
             if (runnableAt > now && !waitingFor) {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index c59ee83..bbffc89 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -344,7 +344,7 @@
         if (AudioService.DEBUG_COMM_RTE) {
             Log.v(TAG, "setCommunicationRouteForClient: device: " + device);
         }
-        AudioService.sDeviceLogger.log((new EventLogger.StringEvent(
+        AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
                                         "setCommunicationRouteForClient for pid: " + pid
                                         + " device: " + device
                                         + " from API: " + eventSource)).printLog(TAG));
@@ -1212,7 +1212,7 @@
         if (useCase == AudioSystem.FOR_MEDIA) {
             postReportNewRoutes(fromA2dp);
         }
-        AudioService.sForceUseLogger.log(
+        AudioService.sForceUseLogger.enqueue(
                 new AudioServiceEvents.ForceUseEvent(useCase, config, eventSource));
         new MediaMetrics.Item(MediaMetrics.Name.AUDIO_FORCE_USE + MediaMetrics.SEPARATOR
                 + AudioSystem.forceUseUsageToString(useCase))
@@ -1230,7 +1230,7 @@
     }
 
     private void onSendBecomingNoisyIntent() {
-        AudioService.sDeviceLogger.log((new EventLogger.StringEvent(
+        AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
                 "broadcast ACTION_AUDIO_BECOMING_NOISY")).printLog(TAG));
         mSystemServer.sendDeviceBecomingNoisyIntent();
     }
@@ -1468,7 +1468,7 @@
                 case MSG_L_BT_ACTIVE_DEVICE_CHANGE_EXT: {
                     final BtDeviceInfo info = (BtDeviceInfo) msg.obj;
                     if (info.mDevice == null) break;
-                    AudioService.sDeviceLogger.log((new EventLogger.StringEvent(
+                    AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
                             "msg: onBluetoothActiveDeviceChange "
                                     + " state=" + info.mState
                                     // only querying address as this is the only readily available
@@ -1858,7 +1858,7 @@
             Log.v(TAG, "onUpdateCommunicationRoute, preferredCommunicationDevice: "
                     + preferredCommunicationDevice + " eventSource: " + eventSource);
         }
-        AudioService.sDeviceLogger.log((new EventLogger.StringEvent(
+        AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
                 "onUpdateCommunicationRoute, preferredCommunicationDevice: "
                 + preferredCommunicationDevice + " eventSource: " + eventSource)));
 
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index ce36ff8..c8f282f 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -309,7 +309,7 @@
             address = "";
         }
 
-        AudioService.sDeviceLogger.log(new EventLogger.StringEvent("BT connected:"
+        AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent("BT connected:"
                         + " addr=" + address
                         + " profile=" + btInfo.mProfile
                         + " state=" + btInfo.mState
@@ -412,13 +412,13 @@
         if (!BluetoothAdapter.checkBluetoothAddress(address)) {
             address = "";
         }
-        AudioService.sDeviceLogger.log(new EventLogger.StringEvent(
+        AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                 "onBluetoothA2dpDeviceConfigChange addr=" + address
                     + " event=" + BtHelper.a2dpDeviceEventToString(event)));
 
         synchronized (mDevicesLock) {
             if (mDeviceBroker.hasScheduledA2dpConnection(btDevice)) {
-                AudioService.sDeviceLogger.log(new EventLogger.StringEvent(
+                AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                         "A2dp config change ignored (scheduled connection change)")
                         .printLog(TAG));
                 mmi.set(MediaMetrics.Property.EARLY_RETURN, "A2dp config change ignored")
@@ -460,7 +460,7 @@
                     BtHelper.getName(btDevice), a2dpCodec);
 
             if (res != AudioSystem.AUDIO_STATUS_OK) {
-                AudioService.sDeviceLogger.log(new EventLogger.StringEvent(
+                AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                         "APM handleDeviceConfigChange failed for A2DP device addr=" + address
                                 + " codec=" + AudioSystem.audioFormatToString(a2dpCodec))
                         .printLog(TAG));
@@ -472,7 +472,7 @@
                                 BluetoothProfile.A2DP, BluetoothProfile.STATE_DISCONNECTED,
                                 musicDevice, AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP));
             } else {
-                AudioService.sDeviceLogger.log(new EventLogger.StringEvent(
+                AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                         "APM handleDeviceConfigChange success for A2DP device addr=" + address
                                 + " codec=" + AudioSystem.audioFormatToString(a2dpCodec))
                         .printLog(TAG));
@@ -522,7 +522,7 @@
                             AudioDeviceInventory.WiredDeviceConnectionState wdcs) {
         int type = wdcs.mAttributes.getInternalType();
 
-        AudioService.sDeviceLogger.log(new AudioServiceEvents.WiredDevConnectEvent(wdcs));
+        AudioService.sDeviceLogger.enqueue(new AudioServiceEvents.WiredDevConnectEvent(wdcs));
 
         MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId
                 + "onSetWiredDeviceConnectionState")
@@ -619,7 +619,7 @@
             @NonNull List<AudioDeviceAttributes> devices) {
         final long identity = Binder.clearCallingIdentity();
 
-        AudioService.sDeviceLogger.log((new EventLogger.StringEvent(
+        AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
                                 "setPreferredDevicesForStrategySync, strategy: " + strategy
                                 + " devices: " + devices)).printLog(TAG));
         final int status = mAudioSystem.setDevicesRoleForStrategy(
@@ -635,7 +635,7 @@
     /*package*/ int removePreferredDevicesForStrategySync(int strategy) {
         final long identity = Binder.clearCallingIdentity();
 
-        AudioService.sDeviceLogger.log((new EventLogger.StringEvent(
+        AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
                 "removePreferredDevicesForStrategySync, strategy: "
                 + strategy)).printLog(TAG));
 
@@ -1000,13 +1000,13 @@
         // TODO: log in MediaMetrics once distinction between connection failure and
         // double connection is made.
         if (res != AudioSystem.AUDIO_STATUS_OK) {
-            AudioService.sDeviceLogger.log(new EventLogger.StringEvent(
+            AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                     "APM failed to make available A2DP device addr=" + address
                             + " error=" + res).printLog(TAG));
             // TODO: connection failed, stop here
             // TODO: return;
         } else {
-            AudioService.sDeviceLogger.log(new EventLogger.StringEvent(
+            AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                     "A2DP device addr=" + address + " now available").printLog(TAG));
         }
 
@@ -1047,7 +1047,7 @@
         if (!deviceToRemoveKey
                 .equals(mApmConnectedDevices.get(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP))) {
             // removing A2DP device not currently used by AudioPolicy, log but don't act on it
-            AudioService.sDeviceLogger.log((new EventLogger.StringEvent(
+            AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
                     "A2DP device " + address + " made unavailable, was not used")).printLog(TAG));
             mmi.set(MediaMetrics.Property.EARLY_RETURN,
                     "A2DP device made unavailable, was not used")
@@ -1062,13 +1062,13 @@
                 AudioSystem.DEVICE_STATE_UNAVAILABLE, a2dpCodec);
 
         if (res != AudioSystem.AUDIO_STATUS_OK) {
-            AudioService.sDeviceLogger.log(new EventLogger.StringEvent(
+            AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                     "APM failed to make unavailable A2DP device addr=" + address
                             + " error=" + res).printLog(TAG));
             // TODO:  failed to disconnect, stop here
             // TODO: return;
         } else {
-            AudioService.sDeviceLogger.log((new EventLogger.StringEvent(
+            AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
                     "A2DP device addr=" + address + " made unavailable")).printLog(TAG));
         }
         mApmConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
@@ -1314,7 +1314,7 @@
                     && !mDeviceBroker.hasAudioFocusUsers()) {
                 // no media playback, not a "becoming noisy" situation, otherwise it could cause
                 // the pausing of some apps that are playing remotely
-                AudioService.sDeviceLogger.log((new EventLogger.StringEvent(
+                AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
                         "dropping ACTION_AUDIO_BECOMING_NOISY")).printLog(TAG));
                 mmi.set(MediaMetrics.Property.DELAY_MS, 0).record(); // OK to return
                 return 0;
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index f3a9a69..9d6fa9e 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -990,7 +990,7 @@
     public AudioService(Context context, AudioSystemAdapter audioSystem,
             SystemServerAdapter systemServer, SettingsAdapter settings, @Nullable Looper looper,
             AppOpsManager appOps) {
-        sLifecycleLogger.log(new EventLogger.StringEvent("AudioService()"));
+        sLifecycleLogger.enqueue(new EventLogger.StringEvent("AudioService()"));
         mContext = context;
         mContentResolver = context.getContentResolver();
         mAppOps = appOps;
@@ -1539,14 +1539,14 @@
         if (!mSystemReady ||
                 (AudioSystem.checkAudioFlinger() != AudioSystem.AUDIO_STATUS_OK)) {
             Log.e(TAG, "Audioserver died.");
-            sLifecycleLogger.log(new EventLogger.StringEvent(
+            sLifecycleLogger.enqueue(new EventLogger.StringEvent(
                     "onAudioServerDied() audioserver died"));
             sendMsg(mAudioHandler, MSG_AUDIO_SERVER_DIED, SENDMSG_NOOP, 0, 0,
                     null, 500);
             return;
         }
         Log.i(TAG, "Audioserver started.");
-        sLifecycleLogger.log(new EventLogger.StringEvent(
+        sLifecycleLogger.enqueue(new EventLogger.StringEvent(
                 "onAudioServerDied() audioserver started"));
 
         updateAudioHalPids();
@@ -1776,7 +1776,7 @@
 
         // did it work? check based on status
         if (status != AudioSystem.AUDIO_STATUS_OK) {
-            sLifecycleLogger.log(new EventLogger.StringEvent(
+            sLifecycleLogger.enqueue(new EventLogger.StringEvent(
                     caller + ": initStreamVolume failed with " + status + " will retry")
                     .printLog(ALOGE, TAG));
             sendMsg(mAudioHandler, MSG_REINIT_VOLUMES, SENDMSG_NOOP, 0, 0,
@@ -1790,7 +1790,7 @@
         }
 
         // success
-        sLifecycleLogger.log(new EventLogger.StringEvent(
+        sLifecycleLogger.enqueue(new EventLogger.StringEvent(
                 caller + ": initStreamVolume succeeded").printLog(ALOGI, TAG));
     }
 
@@ -1813,7 +1813,7 @@
             }
         }
         if (!success) {
-            sLifecycleLogger.log(new EventLogger.StringEvent(
+            sLifecycleLogger.enqueue(new EventLogger.StringEvent(
                     caller + ": initStreamVolume succeeded but invalid mix/max levels, will retry")
                     .printLog(ALOGW, TAG));
             sendMsg(mAudioHandler, MSG_REINIT_VOLUMES, SENDMSG_NOOP, 0, 0,
@@ -2764,7 +2764,7 @@
                 "setPreferredDeviceForStrategy u/pid:%d/%d strat:%d dev:%s",
                 Binder.getCallingUid(), Binder.getCallingPid(), strategy,
                 devices.stream().map(e -> e.toString()).collect(Collectors.joining(",")));
-        sDeviceLogger.log(new EventLogger.StringEvent(logString).printLog(TAG));
+        sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG));
         if (devices.stream().anyMatch(device ->
                 device.getRole() == AudioDeviceAttributes.ROLE_INPUT)) {
             Log.e(TAG, "Unsupported input routing in " + logString);
@@ -2784,7 +2784,7 @@
     public int removePreferredDevicesForStrategy(int strategy) {
         final String logString =
                 String.format("removePreferredDeviceForStrategy strat:%d", strategy);
-        sDeviceLogger.log(new EventLogger.StringEvent(logString).printLog(TAG));
+        sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG));
 
         final int status = mDeviceBroker.removePreferredDevicesForStrategySync(strategy);
         if (status != AudioSystem.SUCCESS) {
@@ -2850,7 +2850,7 @@
                 "setPreferredDevicesForCapturePreset u/pid:%d/%d source:%d dev:%s",
                 Binder.getCallingUid(), Binder.getCallingPid(), capturePreset,
                 devices.stream().map(e -> e.toString()).collect(Collectors.joining(",")));
-        sDeviceLogger.log(new EventLogger.StringEvent(logString).printLog(TAG));
+        sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG));
         if (devices.stream().anyMatch(device ->
                 device.getRole() == AudioDeviceAttributes.ROLE_OUTPUT)) {
             Log.e(TAG, "Unsupported output routing in " + logString);
@@ -2871,7 +2871,7 @@
     public int clearPreferredDevicesForCapturePreset(int capturePreset) {
         final String logString = String.format(
                 "removePreferredDeviceForCapturePreset source:%d", capturePreset);
-        sDeviceLogger.log(new EventLogger.StringEvent(logString).printLog(TAG));
+        sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG));
 
         final int status = mDeviceBroker.clearPreferredDevicesForCapturePresetSync(capturePreset);
         if (status != AudioSystem.SUCCESS) {
@@ -3043,9 +3043,10 @@
                 + ", volControlStream=" + mVolumeControlStream
                 + ", userSelect=" + mUserSelectedVolumeControlStream);
         if (direction != AudioManager.ADJUST_SAME) {
-            sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_ADJUST_SUGG_VOL, suggestedStreamType,
-                    direction/*val1*/, flags/*val2*/, new StringBuilder(callingPackage)
-                    .append("/").append(caller).append(" uid:").append(uid).toString()));
+            sVolumeLogger.enqueue(
+                    new VolumeEvent(VolumeEvent.VOL_ADJUST_SUGG_VOL, suggestedStreamType,
+                            direction/*val1*/, flags/*val2*/, new StringBuilder(callingPackage)
+                            .append("/").append(caller).append(" uid:").append(uid).toString()));
         }
 
         boolean hasExternalVolumeController = notifyExternalVolumeController(direction);
@@ -3144,7 +3145,7 @@
             return;
         }
 
-        sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_ADJUST_STREAM_VOL, streamType,
+        sVolumeLogger.enqueue(new VolumeEvent(VolumeEvent.VOL_ADJUST_STREAM_VOL, streamType,
                 direction/*val1*/, flags/*val2*/, callingPackage));
         adjustStreamVolume(streamType, direction, flags, callingPackage, callingPackage,
                 Binder.getCallingUid(), Binder.getCallingPid(), attributionTag,
@@ -3625,7 +3626,7 @@
         }
         final VolumeGroupState vgs = sVolumeGroupStates.get(volumeGroup);
 
-        sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_GROUP_VOL, attr, vgs.name(),
+        sVolumeLogger.enqueue(new VolumeEvent(VolumeEvent.VOL_SET_GROUP_VOL, attr, vgs.name(),
                 index/*val1*/, flags/*val2*/, callingPackage));
 
         vgs.setVolumeIndex(index, flags);
@@ -3776,7 +3777,7 @@
                 ? new VolumeEvent(VolumeEvent.VOL_SET_STREAM_VOL, streamType,
                     index/*val1*/, flags/*val2*/, callingPackage)
                 : new DeviceVolumeEvent(streamType, index, device, callingPackage);
-        sVolumeLogger.log(event);
+        sVolumeLogger.enqueue(event);
         setStreamVolume(streamType, index, flags, device,
                 callingPackage, callingPackage, attributionTag,
                 Binder.getCallingUid(), callingOrSelfHasAudioSettingsPermission());
@@ -3977,7 +3978,7 @@
     private void updateHearingAidVolumeOnVoiceActivityUpdate() {
         final int streamType = getBluetoothContextualVolumeStream();
         final int index = getStreamVolume(streamType);
-        sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_VOICE_ACTIVITY_HEARING_AID,
+        sVolumeLogger.enqueue(new VolumeEvent(VolumeEvent.VOL_VOICE_ACTIVITY_HEARING_AID,
                 mVoicePlaybackActive.get(), streamType, index));
         mDeviceBroker.postSetHearingAidVolumeIndex(index * 10, streamType);
 
@@ -4018,7 +4019,7 @@
         if (AudioSystem.isSingleAudioDeviceType(
                 absVolumeMultiModeCaseDevices, AudioSystem.DEVICE_OUT_HEARING_AID)) {
             final int index = getStreamVolume(streamType);
-            sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_MODE_CHANGE_HEARING_AID,
+            sVolumeLogger.enqueue(new VolumeEvent(VolumeEvent.VOL_MODE_CHANGE_HEARING_AID,
                     newMode, streamType, index));
             mDeviceBroker.postSetHearingAidVolumeIndex(index * 10, streamType);
         }
@@ -5419,7 +5420,7 @@
                         /*obj*/ null, /*delay*/ 0);
                 int previousMode = mMode.getAndSet(mode);
                 // Note: newModeOwnerPid is always 0 when actualMode is MODE_NORMAL
-                mModeLogger.log(new PhoneStateEvent(requesterPackage, requesterPid,
+                mModeLogger.enqueue(new PhoneStateEvent(requesterPackage, requesterPid,
                         requestedMode, pid, mode));
 
                 int streamType = getActiveStreamType(AudioManager.USE_DEFAULT_STREAM_TYPE);
@@ -5562,7 +5563,7 @@
         }
 
         if (direction != AudioManager.ADJUST_SAME) {
-            sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_ADJUST_VOL_UID, streamType,
+            sVolumeLogger.enqueue(new VolumeEvent(VolumeEvent.VOL_ADJUST_VOL_UID, streamType,
                     direction/*val1*/, flags/*val2*/,
                     new StringBuilder(packageName).append(" uid:").append(uid)
                     .toString()));
@@ -6892,7 +6893,7 @@
         // verify arguments
         Objects.requireNonNull(device);
         AudioManager.enforceValidVolumeBehavior(deviceVolumeBehavior);
-        sVolumeLogger.log(new EventLogger.StringEvent("setDeviceVolumeBehavior: dev:"
+        sVolumeLogger.enqueue(new EventLogger.StringEvent("setDeviceVolumeBehavior: dev:"
                 + AudioSystem.getOutputDeviceName(device.getInternalType()) + " addr:"
                 + device.getAddress() + " behavior:"
                 + AudioDeviceVolumeManager.volumeBehaviorName(deviceVolumeBehavior)
@@ -6948,7 +6949,7 @@
         }
 
         // log event and caller
-        sDeviceLogger.log(new EventLogger.StringEvent(
+        sDeviceLogger.enqueue(new EventLogger.StringEvent(
                 "Volume behavior " + deviceVolumeBehavior + " for dev=0x"
                       + Integer.toHexString(audioSystemDeviceOut) + " from:" + caller));
         // make sure we have a volume entry for this device, and that volume is updated according
@@ -7594,7 +7595,7 @@
             final int status = AudioSystem.initStreamVolume(
                     streamType, mIndexMin / 10, mIndexMax / 10);
             if (status != AudioSystem.AUDIO_STATUS_OK) {
-                sLifecycleLogger.log(new EventLogger.StringEvent(
+                sLifecycleLogger.enqueue(new EventLogger.StringEvent(
                          "VSS() stream:" + streamType + " initStreamVolume=" + status)
                         .printLog(ALOGE, TAG));
                 sendMsg(mAudioHandler, MSG_REINIT_VOLUMES, SENDMSG_NOOP, 0, 0,
@@ -8013,7 +8014,7 @@
                 }
             }
             if (changed) {
-                sVolumeLogger.log(new VolumeEvent(
+                sVolumeLogger.enqueue(new VolumeEvent(
                         VolumeEvent.VOL_MUTE_STREAM_INT, mStreamType, state));
             }
             return changed;
@@ -8183,10 +8184,10 @@
             streamState.setIndex(index, update.mDevice, update.mCaller,
                     // trusted as index is always validated before message is posted
                     true /*hasModifyAudioSettings*/);
-            sVolumeLogger.log(new EventLogger.StringEvent(update.mCaller + " dev:0x"
+            sVolumeLogger.enqueue(new EventLogger.StringEvent(update.mCaller + " dev:0x"
                     + Integer.toHexString(update.mDevice) + " volIdx:" + index));
         } else {
-            sVolumeLogger.log(new EventLogger.StringEvent(update.mCaller
+            sVolumeLogger.enqueue(new EventLogger.StringEvent(update.mCaller
                     + " update vol on dev:0x" + Integer.toHexString(update.mDevice)));
         }
         setDeviceVolume(streamState, update.mDevice);
@@ -8364,7 +8365,7 @@
                             .set(MediaMetrics.Property.FORCE_USE_MODE,
                                     AudioSystem.forceUseConfigToString(config))
                             .record();
-                    sForceUseLogger.log(
+                    sForceUseLogger.enqueue(
                             new AudioServiceEvents.ForceUseEvent(useCase, config, eventSource));
                     mAudioSystem.setForceUse(useCase, config);
                 }
@@ -8633,7 +8634,7 @@
 
     private void avrcpSupportsAbsoluteVolume(String address, boolean support) {
         // address is not used for now, but may be used when multiple a2dp devices are supported
-        sVolumeLogger.log(new EventLogger.StringEvent("avrcpSupportsAbsoluteVolume addr="
+        sVolumeLogger.enqueue(new EventLogger.StringEvent("avrcpSupportsAbsoluteVolume addr="
                 + address + " support=" + support).printLog(TAG));
         mDeviceBroker.setAvrcpAbsoluteVolumeSupported(support);
         setAvrcpAbsoluteVolumeSupported(support);
@@ -8767,13 +8768,21 @@
                     UserInfo userInfo = UserManagerService.getInstance().getUserInfo(userId);
                     killBackgroundUserProcessesWithRecordAudioPermission(userInfo);
                 }
-                UserManagerService.getInstance().setUserRestriction(
-                        UserManager.DISALLOW_RECORD_AUDIO, true, userId);
+                try {
+                    UserManagerService.getInstance().setUserRestriction(
+                            UserManager.DISALLOW_RECORD_AUDIO, true, userId);
+                } catch (IllegalArgumentException e) {
+                    Slog.w(TAG, "Failed to apply DISALLOW_RECORD_AUDIO restriction: " + e);
+                }
             } else if (action.equals(Intent.ACTION_USER_FOREGROUND)) {
                 // Enable audio recording for foreground user/profile
                 int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
-                UserManagerService.getInstance().setUserRestriction(
-                        UserManager.DISALLOW_RECORD_AUDIO, false, userId);
+                try {
+                    UserManagerService.getInstance().setUserRestriction(
+                            UserManager.DISALLOW_RECORD_AUDIO, false, userId);
+                } catch (IllegalArgumentException e) {
+                    Slog.w(TAG, "Failed to apply DISALLOW_RECORD_AUDIO restriction: " + e);
+                }
             } else if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
                 state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
                 if (state == BluetoothAdapter.STATE_OFF ||
@@ -10668,7 +10677,7 @@
                 pcb.asBinder().linkToDeath(app, 0/*flags*/);
 
                 // logging after registration so we have the registration id
-                mDynPolicyLogger.log((new EventLogger.StringEvent("registerAudioPolicy for "
+                mDynPolicyLogger.enqueue((new EventLogger.StringEvent("registerAudioPolicy for "
                         + pcb.asBinder() + " u/pid:" + Binder.getCallingUid() + "/"
                         + Binder.getCallingPid() + " with config:" + app.toCompactLogString()))
                         .printLog(TAG));
@@ -10866,7 +10875,7 @@
 
 
     private void unregisterAudioPolicyInt(@NonNull IAudioPolicyCallback pcb, String operationName) {
-        mDynPolicyLogger.log((new EventLogger.StringEvent(operationName + " for "
+        mDynPolicyLogger.enqueue((new EventLogger.StringEvent(operationName + " for "
                 + pcb.asBinder()).printLog(TAG)));
         synchronized (mAudioPolicies) {
             AudioPolicyProxy app = mAudioPolicies.remove(pcb.asBinder());
@@ -11394,7 +11403,7 @@
         }
 
         public void binderDied() {
-            mDynPolicyLogger.log((new EventLogger.StringEvent("AudioPolicy "
+            mDynPolicyLogger.enqueue((new EventLogger.StringEvent("AudioPolicy "
                     + mPolicyCallback.asBinder() + " died").printLog(TAG)));
             release();
         }
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 399829e..df65dbd 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -263,20 +263,20 @@
     /*package*/ synchronized void setAvrcpAbsoluteVolumeIndex(int index) {
         if (mA2dp == null) {
             if (AudioService.DEBUG_VOL) {
-                AudioService.sVolumeLogger.log(new EventLogger.StringEvent(
+                AudioService.sVolumeLogger.enqueue(new EventLogger.StringEvent(
                         "setAvrcpAbsoluteVolumeIndex: bailing due to null mA2dp").printLog(TAG));
                 return;
             }
         }
         if (!mAvrcpAbsVolSupported) {
-            AudioService.sVolumeLogger.log(new EventLogger.StringEvent(
+            AudioService.sVolumeLogger.enqueue(new EventLogger.StringEvent(
                     "setAvrcpAbsoluteVolumeIndex: abs vol not supported ").printLog(TAG));
             return;
         }
         if (AudioService.DEBUG_VOL) {
             Log.i(TAG, "setAvrcpAbsoluteVolumeIndex index=" + index);
         }
-        AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent(
+        AudioService.sVolumeLogger.enqueue(new AudioServiceEvents.VolumeEvent(
                 AudioServiceEvents.VolumeEvent.VOL_SET_AVRCP_VOL, index));
         mA2dp.setAvrcpAbsoluteVolume(index);
     }
@@ -393,14 +393,14 @@
     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
     /*package*/ synchronized boolean startBluetoothSco(int scoAudioMode,
                 @NonNull String eventSource) {
-        AudioService.sDeviceLogger.log(new EventLogger.StringEvent(eventSource));
+        AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(eventSource));
         return requestScoState(BluetoothHeadset.STATE_AUDIO_CONNECTED, scoAudioMode);
     }
 
     // @GuardedBy("AudioDeviceBroker.mSetModeLock")
     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
     /*package*/ synchronized boolean stopBluetoothSco(@NonNull String eventSource) {
-        AudioService.sDeviceLogger.log(new EventLogger.StringEvent(eventSource));
+        AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(eventSource));
         return requestScoState(BluetoothHeadset.STATE_AUDIO_DISCONNECTED, SCO_MODE_VIRTUAL_CALL);
     }
 
@@ -418,7 +418,7 @@
             Log.i(TAG, "setLeAudioVolume: calling mLeAudio.setVolume idx="
                     + index + " volume=" + volume);
         }
-        AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent(
+        AudioService.sVolumeLogger.enqueue(new AudioServiceEvents.VolumeEvent(
                 AudioServiceEvents.VolumeEvent.VOL_SET_LE_AUDIO_VOL, index, maxIndex));
         mLeAudio.setVolume(volume);
     }
@@ -443,7 +443,7 @@
         }
         // do not log when hearing aid is not connected to avoid confusion when reading dumpsys
         if (isHeadAidConnected) {
-            AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent(
+            AudioService.sVolumeLogger.enqueue(new AudioServiceEvents.VolumeEvent(
                     AudioServiceEvents.VolumeEvent.VOL_SET_HEARING_AID_VOL, index, gainDB));
         }
         mHearingAid.setVolume(gainDB);
@@ -675,7 +675,7 @@
                         case BluetoothProfile.HEADSET:
                         case BluetoothProfile.HEARING_AID:
                         case BluetoothProfile.LE_AUDIO:
-                            AudioService.sDeviceLogger.log(new EventLogger.StringEvent(
+                            AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                                     "BT profile service: connecting "
                                     + BluetoothProfile.getProfileName(profile) + " profile"));
                             mDeviceBroker.postBtProfileConnected(profile, proxy);
diff --git a/services/core/java/com/android/server/audio/FadeOutManager.java b/services/core/java/com/android/server/audio/FadeOutManager.java
index e54ee86..5f6f4b1 100644
--- a/services/core/java/com/android/server/audio/FadeOutManager.java
+++ b/services/core/java/com/android/server/audio/FadeOutManager.java
@@ -245,7 +245,7 @@
                 return;
             }
             try {
-                PlaybackActivityMonitor.sEventLogger.log(
+                PlaybackActivityMonitor.sEventLogger.enqueue(
                         (new PlaybackActivityMonitor.FadeOutEvent(apc, skipRamp)).printLog(TAG));
                 apc.getPlayerProxy().applyVolumeShaper(
                         FADEOUT_VSHAPE,
@@ -262,7 +262,7 @@
                 final AudioPlaybackConfiguration apc = players.get(piid);
                 if (apc != null) {
                     try {
-                        PlaybackActivityMonitor.sEventLogger.log(
+                        PlaybackActivityMonitor.sEventLogger.enqueue(
                                 (new EventLogger.StringEvent("unfading out piid:"
                                         + piid)).printLog(TAG));
                         apc.getPlayerProxy().applyVolumeShaper(
diff --git a/services/core/java/com/android/server/audio/MediaFocusControl.java b/services/core/java/com/android/server/audio/MediaFocusControl.java
index 1ca27dd..27687b2 100644
--- a/services/core/java/com/android/server/audio/MediaFocusControl.java
+++ b/services/core/java/com/android/server/audio/MediaFocusControl.java
@@ -185,7 +185,7 @@
                 final FocusRequester focusOwner = stackIterator.next();
                 if (focusOwner.hasSameUid(uid) && focusOwner.hasSamePackage(packageName)) {
                     clientsToRemove.add(focusOwner.getClientId());
-                    mEventLogger.log((new EventLogger.StringEvent(
+                    mEventLogger.enqueue((new EventLogger.StringEvent(
                             "focus owner:" + focusOwner.getClientId()
                                     + " in uid:" + uid + " pack: " + packageName
                                     + " getting AUDIOFOCUS_LOSS due to app suspension"))
@@ -433,7 +433,7 @@
             FocusRequester fr = stackIterator.next();
             if(fr.hasSameBinder(cb)) {
                 Log.i(TAG, "AudioFocus  removeFocusStackEntryOnDeath(): removing entry for " + cb);
-                mEventLogger.log(new EventLogger.StringEvent(
+                mEventLogger.enqueue(new EventLogger.StringEvent(
                         "focus requester:" + fr.getClientId()
                                 + " in uid:" + fr.getClientUid()
                                 + " pack:" + fr.getPackageName()
@@ -470,7 +470,7 @@
             final FocusRequester fr = owner.getValue();
             if (fr.hasSameBinder(cb)) {
                 ownerIterator.remove();
-                mEventLogger.log(new EventLogger.StringEvent(
+                mEventLogger.enqueue(new EventLogger.StringEvent(
                         "focus requester:" + fr.getClientId()
                                 + " in uid:" + fr.getClientUid()
                                 + " pack:" + fr.getPackageName()
@@ -968,7 +968,7 @@
         // supposed to be alone in bitfield
         final int uid = (flags == AudioManager.AUDIOFOCUS_FLAG_TEST)
                 ? testUid : Binder.getCallingUid();
-        mEventLogger.log((new EventLogger.StringEvent(
+        mEventLogger.enqueue((new EventLogger.StringEvent(
                 "requestAudioFocus() from uid/pid " + uid
                     + "/" + Binder.getCallingPid()
                     + " AA=" + aa.usageToString() + "/" + aa.contentTypeToString()
@@ -1143,7 +1143,7 @@
                 .record();
 
         // AudioAttributes are currently ignored, to be used for zones / a11y
-        mEventLogger.log((new EventLogger.StringEvent(
+        mEventLogger.enqueue((new EventLogger.StringEvent(
                 "abandonAudioFocus() from uid/pid " + Binder.getCallingUid()
                     + "/" + Binder.getCallingPid()
                     + " clientId=" + clientId))
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index 1af8c59..74bfa80 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -157,7 +157,7 @@
             if (index >= 0) {
                 if (!disable) {
                     if (DEBUG) { // hidden behind DEBUG, too noisy otherwise
-                        sEventLogger.log(new EventLogger.StringEvent("unbanning uid:" + uid));
+                        sEventLogger.enqueue(new EventLogger.StringEvent("unbanning uid:" + uid));
                     }
                     mBannedUids.remove(index);
                     // nothing else to do, future playback requests from this uid are ok
@@ -168,7 +168,7 @@
                         checkBanPlayer(apc, uid);
                     }
                     if (DEBUG) { // hidden behind DEBUG, too noisy otherwise
-                        sEventLogger.log(new EventLogger.StringEvent("banning uid:" + uid));
+                        sEventLogger.enqueue(new EventLogger.StringEvent("banning uid:" + uid));
                     }
                     mBannedUids.add(new Integer(uid));
                 } // no else to handle, uid already not in list, so enabling again is no-op
@@ -209,7 +209,7 @@
                 updateAllowedCapturePolicy(apc, mAllowedCapturePolicies.get(uid));
             }
         }
-        sEventLogger.log(new NewPlayerEvent(apc));
+        sEventLogger.enqueue(new NewPlayerEvent(apc));
         synchronized(mPlayerLock) {
             mPlayers.put(newPiid, apc);
             maybeMutePlayerAwaitingConnection(apc);
@@ -229,7 +229,7 @@
         synchronized(mPlayerLock) {
             final AudioPlaybackConfiguration apc = mPlayers.get(new Integer(piid));
             if (checkConfigurationCaller(piid, apc, binderUid)) {
-                sEventLogger.log(new AudioAttrEvent(piid, attr));
+                sEventLogger.enqueue(new AudioAttrEvent(piid, attr));
                 change = apc.handleAudioAttributesEvent(attr);
             } else {
                 Log.e(TAG, "Error updating audio attributes");
@@ -322,7 +322,7 @@
                 return;
             }
 
-            sEventLogger.log(new PlayerEvent(piid, event, eventValue));
+            sEventLogger.enqueue(new PlayerEvent(piid, event, eventValue));
 
             if (event == AudioPlaybackConfiguration.PLAYER_UPDATE_PORT_ID) {
                 mEventHandler.sendMessage(
@@ -332,7 +332,7 @@
                 for (Integer uidInteger: mBannedUids) {
                     if (checkBanPlayer(apc, uidInteger.intValue())) {
                         // player was banned, do not update its state
-                        sEventLogger.log(new EventLogger.StringEvent(
+                        sEventLogger.enqueue(new EventLogger.StringEvent(
                                 "not starting piid:" + piid + " ,is banned"));
                         return;
                     }
@@ -412,7 +412,7 @@
 
     public void playerHasOpPlayAudio(int piid, boolean hasOpPlayAudio, int binderUid) {
         // no check on UID yet because this is only for logging at the moment
-        sEventLogger.log(new PlayerOpPlayAudioEvent(piid, hasOpPlayAudio, binderUid));
+        sEventLogger.enqueue(new PlayerOpPlayAudioEvent(piid, hasOpPlayAudio, binderUid));
     }
 
     public void releasePlayer(int piid, int binderUid) {
@@ -421,7 +421,7 @@
         synchronized(mPlayerLock) {
             final AudioPlaybackConfiguration apc = mPlayers.get(new Integer(piid));
             if (checkConfigurationCaller(piid, apc, binderUid)) {
-                sEventLogger.log(new EventLogger.StringEvent(
+                sEventLogger.enqueue(new EventLogger.StringEvent(
                         "releasing player piid:" + piid));
                 mPlayers.remove(new Integer(piid));
                 mDuckingManager.removeReleased(apc);
@@ -443,7 +443,7 @@
     }
 
     /*package*/ void onAudioServerDied() {
-        sEventLogger.log(
+        sEventLogger.enqueue(
                 new EventLogger.StringEvent(
                         "clear port id to piid map"));
         synchronized (mPlayerLock) {
@@ -768,7 +768,7 @@
                 }
                 if (mute) {
                     try {
-                        sEventLogger.log((new EventLogger.StringEvent("call: muting piid:"
+                        sEventLogger.enqueue((new EventLogger.StringEvent("call: muting piid:"
                                 + piid + " uid:" + apc.getClientUid())).printLog(TAG));
                         apc.getPlayerProxy().setVolume(0.0f);
                         mMutedPlayers.add(new Integer(piid));
@@ -793,7 +793,7 @@
                 final AudioPlaybackConfiguration apc = mPlayers.get(piid);
                 if (apc != null) {
                     try {
-                        sEventLogger.log(new EventLogger.StringEvent("call: unmuting piid:"
+                        sEventLogger.enqueue(new EventLogger.StringEvent("call: unmuting piid:"
                                 + piid).printLog(TAG));
                         apc.getPlayerProxy().setVolume(1.0f);
                     } catch (Exception e) {
@@ -1081,7 +1081,7 @@
                     return;
                 }
                 try {
-                    sEventLogger.log((new DuckEvent(apc, skipRamp)).printLog(TAG));
+                    sEventLogger.enqueue((new DuckEvent(apc, skipRamp)).printLog(TAG));
                     apc.getPlayerProxy().applyVolumeShaper(
                             DUCK_VSHAPE,
                             skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED);
@@ -1096,7 +1096,7 @@
                     final AudioPlaybackConfiguration apc = players.get(piid);
                     if (apc != null) {
                         try {
-                            sEventLogger.log((new EventLogger.StringEvent("unducking piid:"
+                            sEventLogger.enqueue((new EventLogger.StringEvent("unducking piid:"
                                     + piid)).printLog(TAG));
                             apc.getPlayerProxy().applyVolumeShaper(
                                     DUCK_ID,
@@ -1310,8 +1310,9 @@
     //==========================================================================================
     void muteAwaitConnection(@NonNull int[] usagesToMute,
             @NonNull AudioDeviceAttributes dev, long timeOutMs) {
-        sEventLogger.loglogi(
-                "muteAwaitConnection() dev:" + dev + " timeOutMs:" + timeOutMs, TAG);
+        sEventLogger.enqueueAndLog(
+                "muteAwaitConnection() dev:" + dev + " timeOutMs:" + timeOutMs,
+                EventLogger.Event.ALOGI, TAG);
         synchronized (mPlayerLock) {
             mutePlayersExpectingDevice(usagesToMute);
             // schedule timeout (remove previously scheduled first)
@@ -1323,7 +1324,8 @@
     }
 
     void cancelMuteAwaitConnection(String source) {
-        sEventLogger.loglogi("cancelMuteAwaitConnection() from:" + source, TAG);
+        sEventLogger.enqueueAndLog("cancelMuteAwaitConnection() from:" + source,
+                EventLogger.Event.ALOGI, TAG);
         synchronized (mPlayerLock) {
             // cancel scheduled timeout, ignore device, only one expected device at a time
             mEventHandler.removeMessages(MSG_L_TIMEOUT_MUTE_AWAIT_CONNECTION);
@@ -1346,7 +1348,7 @@
 
     @GuardedBy("mPlayerLock")
     private void mutePlayersExpectingDevice(@NonNull int[] usagesToMute) {
-        sEventLogger.log(new MuteAwaitConnectionEvent(usagesToMute));
+        sEventLogger.enqueue(new MuteAwaitConnectionEvent(usagesToMute));
         mMutedUsagesAwaitingConnection = usagesToMute;
         final Set<Integer> piidSet = mPlayers.keySet();
         final Iterator<Integer> piidIterator = piidSet.iterator();
@@ -1369,7 +1371,7 @@
         for (int usage : mMutedUsagesAwaitingConnection) {
             if (usage == apc.getAudioAttributes().getUsage()) {
                 try {
-                    sEventLogger.log((new EventLogger.StringEvent(
+                    sEventLogger.enqueue((new EventLogger.StringEvent(
                             "awaiting connection: muting piid:"
                                     + apc.getPlayerInterfaceId()
                                     + " uid:" + apc.getClientUid())).printLog(TAG));
@@ -1394,7 +1396,7 @@
                 continue;
             }
             try {
-                sEventLogger.log(new EventLogger.StringEvent(
+                sEventLogger.enqueue(new EventLogger.StringEvent(
                         "unmuting piid:" + piid).printLog(TAG));
                 apc.getPlayerProxy().applyVolumeShaper(MUTE_AWAIT_CONNECTION_VSHAPE,
                         VolumeShaper.Operation.REVERSE);
@@ -1452,8 +1454,9 @@
             public void handleMessage(Message msg) {
                 switch (msg.what) {
                     case MSG_L_TIMEOUT_MUTE_AWAIT_CONNECTION:
-                        sEventLogger.loglogi("Timeout for muting waiting for "
-                                + (AudioDeviceAttributes) msg.obj + ", unmuting", TAG);
+                        sEventLogger.enqueueAndLog("Timeout for muting waiting for "
+                                + (AudioDeviceAttributes) msg.obj + ", unmuting",
+                                EventLogger.Event.ALOGI, TAG);
                         synchronized (mPlayerLock) {
                             unmutePlayersExpectingDevice();
                         }
@@ -1476,7 +1479,7 @@
                         synchronized (mPlayerLock) {
                             int piid = msg.arg1;
 
-                            sEventLogger.log(
+                            sEventLogger.enqueue(
                                     new PlayerEvent(piid, PLAYER_UPDATE_MUTED, eventValue));
 
                             final AudioPlaybackConfiguration apc = mPlayers.get(piid);
diff --git a/services/core/java/com/android/server/audio/RecordingActivityMonitor.java b/services/core/java/com/android/server/audio/RecordingActivityMonitor.java
index 2ba8882..652ea52 100644
--- a/services/core/java/com/android/server/audio/RecordingActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/RecordingActivityMonitor.java
@@ -164,7 +164,7 @@
         }
         if (MediaRecorder.isSystemOnlyAudioSource(source)) {
             // still want to log event, it just won't appear in recording configurations;
-            sEventLogger.log(new RecordingEvent(event, riid, config).printLog(TAG));
+            sEventLogger.enqueue(new RecordingEvent(event, riid, config).printLog(TAG));
             return;
         }
         dispatchCallbacks(updateSnapshot(event, riid, config));
@@ -204,7 +204,7 @@
                 ? AudioManager.RECORD_CONFIG_EVENT_STOP : AudioManager.RECORD_CONFIG_EVENT_NONE;
         if (riid == AudioManager.RECORD_RIID_INVALID
                 || configEvent == AudioManager.RECORD_CONFIG_EVENT_NONE) {
-            sEventLogger.log(new RecordingEvent(event, riid, null).printLog(TAG));
+            sEventLogger.enqueue(new RecordingEvent(event, riid, null).printLog(TAG));
             return;
         }
         dispatchCallbacks(updateSnapshot(configEvent, riid, null));
@@ -301,7 +301,7 @@
                 if (!state.hasDeathHandler()) {
                     if (state.isActiveConfiguration()) {
                         configChanged = true;
-                        sEventLogger.log(new RecordingEvent(
+                        sEventLogger.enqueue(new RecordingEvent(
                                         AudioManager.RECORD_CONFIG_EVENT_RELEASE,
                                         state.getRiid(), state.getConfig()));
                     }
@@ -486,7 +486,7 @@
                     configChanged = false;
             }
             if (configChanged) {
-                sEventLogger.log(new RecordingEvent(event, riid, state.getConfig()));
+                sEventLogger.enqueue(new RecordingEvent(event, riid, state.getConfig()));
                 configs = getActiveRecordingConfigurations(true /*isPrivileged*/);
             }
         }
diff --git a/services/core/java/com/android/server/audio/SoundEffectsHelper.java b/services/core/java/com/android/server/audio/SoundEffectsHelper.java
index 93eba50..79b54eb 100644
--- a/services/core/java/com/android/server/audio/SoundEffectsHelper.java
+++ b/services/core/java/com/android/server/audio/SoundEffectsHelper.java
@@ -164,7 +164,7 @@
     }
 
     private void logEvent(String msg) {
-        mSfxLogger.log(new EventLogger.StringEvent(msg));
+        mSfxLogger.enqueue(new EventLogger.StringEvent(msg));
     }
 
     // All the methods below run on the worker thread
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index 1563d33..2b525f1 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -1708,11 +1708,11 @@
 
 
     private static void loglogi(String msg) {
-        AudioService.sSpatialLogger.loglogi(msg, TAG);
+        AudioService.sSpatialLogger.enqueueAndLog(msg, EventLogger.Event.ALOGI, TAG);
     }
 
     private static String logloge(String msg) {
-        AudioService.sSpatialLogger.loglog(msg, EventLogger.Event.ALOGE, TAG);
+        AudioService.sSpatialLogger.enqueueAndLog(msg, EventLogger.Event.ALOGE, TAG);
         return msg;
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java b/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java
index aeb6b6e..969a174 100644
--- a/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java
+++ b/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.hardware.biometrics.BiometricOverlayConstants;
 import android.hardware.fingerprint.ISidefpsController;
+import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback;
 import android.os.RemoteException;
@@ -43,6 +44,7 @@
 
     @NonNull private final Optional<IUdfpsOverlayController> mUdfpsOverlayController;
     @NonNull private final Optional<ISidefpsController> mSidefpsController;
+    @NonNull private final Optional<IUdfpsOverlay> mUdfpsOverlay;
 
     /**
      * Create an overlay controller for each modality.
@@ -52,9 +54,11 @@
      */
     public SensorOverlays(
             @Nullable IUdfpsOverlayController udfpsOverlayController,
-            @Nullable ISidefpsController sidefpsController) {
+            @Nullable ISidefpsController sidefpsController,
+            @Nullable IUdfpsOverlay udfpsOverlay) {
         mUdfpsOverlayController = Optional.ofNullable(udfpsOverlayController);
         mSidefpsController = Optional.ofNullable(sidefpsController);
+        mUdfpsOverlay = Optional.ofNullable(udfpsOverlay);
     }
 
     /**
@@ -90,6 +94,14 @@
                 Slog.e(TAG, "Remote exception when showing the UDFPS overlay", e);
             }
         }
+
+        if (mUdfpsOverlay.isPresent()) {
+            try {
+                mUdfpsOverlay.get().show(client.getRequestId(), sensorId, reason);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote exception when showing the new UDFPS overlay", e);
+            }
+        }
     }
 
     /**
@@ -113,6 +125,14 @@
                 Slog.e(TAG, "Remote exception when hiding the UDFPS overlay", e);
             }
         }
+
+        if (mUdfpsOverlay.isPresent()) {
+            try {
+                mUdfpsOverlay.get().hide(sensorId);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote exception when hiding the new udfps overlay", e);
+            }
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
index b0dc28d..156e6bb 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
@@ -52,6 +52,7 @@
 import android.hardware.fingerprint.IFingerprintService;
 import android.hardware.fingerprint.IFingerprintServiceReceiver;
 import android.hardware.fingerprint.ISidefpsController;
+import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.Binder;
 import android.os.Build;
@@ -874,6 +875,14 @@
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
+        public void setUdfpsOverlay(@NonNull IUdfpsOverlay controller) {
+            for (ServiceProvider provider : mRegistry.getProviders()) {
+                provider.setUdfpsOverlay(controller);
+            }
+        }
+
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+        @Override
         public void onPowerPressed() {
             for (ServiceProvider provider : mRegistry.getProviders()) {
                 provider.onPowerPressed();
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
index 0c29f56..05c2e29 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
@@ -26,6 +26,7 @@
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IFingerprintServiceReceiver;
 import android.hardware.fingerprint.ISidefpsController;
+import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.IBinder;
 
@@ -129,6 +130,12 @@
 
     void setUdfpsOverlayController(@NonNull IUdfpsOverlayController controller);
 
+    /**
+     * Sets udfps overlay
+     * @param controller udfps overlay
+     */
+    void setUdfpsOverlay(@NonNull IUdfpsOverlay controller);
+
     void onPowerPressed();
 
     /**
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 2e5663d..7f1fb1c 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
@@ -33,6 +33,7 @@
 import android.hardware.biometrics.fingerprint.PointerContext;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.ISidefpsController;
+import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.Build;
 import android.os.Handler;
@@ -118,6 +119,7 @@
             @NonNull LockoutCache lockoutCache,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
             @Nullable ISidefpsController sidefpsController,
+            @Nullable IUdfpsOverlay udfpsOverlay,
             boolean allowBackgroundAuthentication,
             @NonNull FingerprintSensorPropertiesInternal sensorProps,
             @NonNull Handler handler,
@@ -145,7 +147,8 @@
                 false /* isKeyguardBypassEnabled */);
         setRequestId(requestId);
         mLockoutCache = lockoutCache;
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController,
+                sidefpsController, udfpsOverlay);
         mSensorProps = sensorProps;
         mALSProbeCallback = getLogger().getAmbientLightProbe(false /* startWithClient */);
         mHandler = handler;
@@ -248,8 +251,8 @@
                     if (authenticated && mSensorProps.isAnySidefpsType()) {
                         if (mHandler.hasMessages(MESSAGE_IGNORE_AUTH)) {
                             Slog.i(TAG, "(sideFPS) Ignoring auth due to recent power press");
-                            onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED, 0,
-                                    true);
+                            onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED,
+                                    0, true);
                             return;
                         }
                         delay = isKeyguard() ? mWaitForAuthKeyguard : mWaitForAuthBp;
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
index 0e89814..5282234 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.hardware.biometrics.BiometricOverlayConstants;
 import android.hardware.biometrics.common.ICancellationSignal;
+import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -54,12 +55,15 @@
             @NonNull ClientMonitorCallbackConverter listener, int userId,
             @NonNull String owner, int sensorId,
             @NonNull BiometricLogger biometricLogger, @NonNull BiometricContext biometricContext,
-            @Nullable IUdfpsOverlayController udfpsOverlayController, boolean isStrongBiometric) {
+            @Nullable IUdfpsOverlayController udfpsOverlayController,
+            @Nullable IUdfpsOverlay udfpsOverlay,
+            boolean isStrongBiometric) {
         super(context, lazyDaemon, token, listener, userId, owner, 0 /* cookie */, sensorId,
                 true /* shouldVibrate */, biometricLogger, biometricContext);
         setRequestId(requestId);
         mIsStrongBiometric = isStrongBiometric;
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController, null /* sideFpsController*/);
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController,
+                null /* sideFpsController*/, udfpsOverlay);
     }
 
     @Override
@@ -82,7 +86,8 @@
 
     @Override
     protected void startHalOperation() {
-        mSensorOverlays.show(getSensorId(), BiometricOverlayConstants.REASON_AUTH_KEYGUARD, this);
+        mSensorOverlays.show(getSensorId(), BiometricOverlayConstants.REASON_AUTH_KEYGUARD,
+                this);
 
         try {
             mCancellationSignal = doDetectInteraction();
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
index 612d906..7e5d39f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
@@ -30,6 +30,7 @@
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.ISidefpsController;
+import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.hardware.keymaster.HardwareAuthToken;
 import android.os.IBinder;
@@ -86,6 +87,7 @@
             @NonNull FingerprintSensorPropertiesInternal sensorProps,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
             @Nullable ISidefpsController sidefpsController,
+            @Nullable IUdfpsOverlay udfpsOverlay,
             int maxTemplatesPerUser, @FingerprintManager.EnrollReason int enrollReason) {
         // UDFPS haptics occur when an image is acquired (instead of when the result is known)
         super(context, lazyDaemon, token, listener, userId, hardwareAuthToken, owner, utils,
@@ -93,7 +95,8 @@
                 biometricContext);
         setRequestId(requestId);
         mSensorProps = sensorProps;
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController,
+                sidefpsController, udfpsOverlay);
         mMaxTemplatesPerUser = maxTemplatesPerUser;
 
         mALSProbeCallback = getLogger().getAmbientLightProbe(true /* startWithClient */);
@@ -162,7 +165,8 @@
 
     @Override
     protected void startHalOperation() {
-        mSensorOverlays.show(getSensorId(), getOverlayReasonFromEnrollReason(mEnrollReason), this);
+        mSensorOverlays.show(getSensorId(), getOverlayReasonFromEnrollReason(mEnrollReason),
+                this);
 
         BiometricNotificationUtils.cancelBadCalibrationNotification(getContext());
         try {
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 628c16a..a42ff9a 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
@@ -40,6 +40,7 @@
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IFingerprintServiceReceiver;
 import android.hardware.fingerprint.ISidefpsController;
+import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.Binder;
 import android.os.Handler;
@@ -108,6 +109,7 @@
     @Nullable private IFingerprint mDaemon;
     @Nullable private IUdfpsOverlayController mUdfpsOverlayController;
     @Nullable private ISidefpsController mSidefpsController;
+    @Nullable private IUdfpsOverlay mUdfpsOverlay;
 
     private final class BiometricTaskStackListener extends TaskStackListener {
         @Override
@@ -383,7 +385,8 @@
                             BiometricsProtoEnums.CLIENT_UNKNOWN),
                     mBiometricContext,
                     mSensors.get(sensorId).getSensorProperties(),
-                    mUdfpsOverlayController, mSidefpsController, maxTemplatesPerUser, enrollReason);
+                    mUdfpsOverlayController, mSidefpsController, mUdfpsOverlay,
+                    maxTemplatesPerUser, enrollReason);
             scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(
                     mBiometricStateCallback, new ClientMonitorCallback() {
                 @Override
@@ -418,7 +421,7 @@
                     opPackageName, sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
                     mBiometricContext,
-                    mUdfpsOverlayController, isStrongBiometric);
+                    mUdfpsOverlayController, mUdfpsOverlay, isStrongBiometric);
             scheduleForSensor(sensorId, client, mBiometricStateCallback);
         });
 
@@ -439,10 +442,10 @@
                     createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
                     mBiometricContext, isStrongBiometric,
                     mTaskStackListener, mSensors.get(sensorId).getLockoutCache(),
-                    mUdfpsOverlayController, mSidefpsController, allowBackgroundAuthentication,
+                    mUdfpsOverlayController, mSidefpsController, mUdfpsOverlay,
+                    allowBackgroundAuthentication,
                     mSensors.get(sensorId).getSensorProperties(), mHandler,
-                    Utils.getCurrentStrength(sensorId),
-                    SystemClock.elapsedRealtimeClock());
+                    Utils.getCurrentStrength(sensorId), SystemClock.elapsedRealtimeClock());
             scheduleForSensor(sensorId, client, mBiometricStateCallback);
         });
     }
@@ -649,6 +652,11 @@
     }
 
     @Override
+    public void setUdfpsOverlay(@NonNull IUdfpsOverlay controller) {
+        mUdfpsOverlay = controller;
+    }
+
+    @Override
     public void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto,
             boolean clearSchedulerBuffer) {
         if (mSensors.contains(sensorId)) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
index 0e6df8e..dbc96df 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
@@ -38,6 +38,7 @@
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IFingerprintServiceReceiver;
 import android.hardware.fingerprint.ISidefpsController;
+import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.Handler;
 import android.os.IBinder;
@@ -120,6 +121,7 @@
     @NonNull private final HalResultController mHalResultController;
     @Nullable private IUdfpsOverlayController mUdfpsOverlayController;
     @Nullable private ISidefpsController mSidefpsController;
+    @Nullable private IUdfpsOverlay mUdfpsOverlay;
     @NonNull private final BiometricContext mBiometricContext;
     // for requests that do not use biometric prompt
     @NonNull private final AtomicLong mRequestCounter = new AtomicLong(0);
@@ -594,7 +596,7 @@
                     createLogger(BiometricsProtoEnums.ACTION_ENROLL,
                             BiometricsProtoEnums.CLIENT_UNKNOWN),
                     mBiometricContext,
-                    mUdfpsOverlayController, mSidefpsController,
+                    mUdfpsOverlayController, mSidefpsController, mUdfpsOverlay,
                     enrollReason);
             mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() {
                 @Override
@@ -640,7 +642,7 @@
                     mLazyDaemon, token, id, listener, userId, opPackageName,
                     mSensorProperties.sensorId,
                     createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
-                    mBiometricContext, mUdfpsOverlayController,
+                    mBiometricContext, mUdfpsOverlayController, mUdfpsOverlay,
                     isStrongBiometric);
             mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
         });
@@ -664,7 +666,7 @@
                     createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
                     mBiometricContext, isStrongBiometric,
                     mTaskStackListener, mLockoutTracker,
-                    mUdfpsOverlayController, mSidefpsController,
+                    mUdfpsOverlayController, mSidefpsController, mUdfpsOverlay,
                     allowBackgroundAuthentication, mSensorProperties);
             mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
         });
@@ -853,6 +855,11 @@
     }
 
     @Override
+    public void setUdfpsOverlay(@NonNull IUdfpsOverlay controller) {
+        mUdfpsOverlay = controller;
+    }
+
+    @Override
     public void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto,
             boolean clearSchedulerBuffer) {
         final long sensorToken = proto.start(SensorServiceStateProto.SENSOR_STATES);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
index 0d620fd..56fa36e 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
@@ -26,6 +26,7 @@
 import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.ISidefpsController;
+import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -76,15 +77,18 @@
             @NonNull LockoutFrameworkImpl lockoutTracker,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
             @Nullable ISidefpsController sidefpsController,
+            @Nullable IUdfpsOverlay udfpsOverlay,
             boolean allowBackgroundAuthentication,
             @NonNull FingerprintSensorPropertiesInternal sensorProps) {
         super(context, lazyDaemon, token, listener, targetUserId, operationId, restricted,
                 owner, cookie, requireConfirmation, sensorId, logger, biometricContext,
-                isStrongBiometric, taskStackListener, lockoutTracker, allowBackgroundAuthentication,
-                false /* shouldVibrate */, false /* isKeyguardBypassEnabled */);
+                isStrongBiometric, taskStackListener, lockoutTracker,
+                allowBackgroundAuthentication, false /* shouldVibrate */,
+                false /* isKeyguardBypassEnabled */);
         setRequestId(requestId);
         mLockoutFrameworkImpl = lockoutTracker;
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController,
+                sidefpsController, udfpsOverlay);
         mSensorProps = sensorProps;
         mALSProbeCallback = getLogger().getAmbientLightProbe(false /* startWithClient */);
     }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
index c2929d0..3e9b8ef 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java
@@ -23,6 +23,7 @@
 import android.hardware.biometrics.BiometricFingerprintConstants;
 import android.hardware.biometrics.BiometricOverlayConstants;
 import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
+import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -62,11 +63,13 @@
             @NonNull ClientMonitorCallbackConverter listener, int userId, @NonNull String owner,
             int sensorId,
             @NonNull BiometricLogger biometricLogger, @NonNull BiometricContext biometricContext,
-            @Nullable IUdfpsOverlayController udfpsOverlayController, boolean isStrongBiometric) {
+            @Nullable IUdfpsOverlayController udfpsOverlayController,
+            @Nullable IUdfpsOverlay udfpsOverlay, boolean isStrongBiometric) {
         super(context, lazyDaemon, token, listener, userId, owner, 0 /* cookie */, sensorId,
                 true /* shouldVibrate */, biometricLogger, biometricContext);
         setRequestId(requestId);
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController, null /* sideFpsController */);
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController,
+                null /* sideFpsController */, udfpsOverlay);
         mIsStrongBiometric = isStrongBiometric;
     }
 
@@ -92,7 +95,8 @@
 
     @Override
     protected void startHalOperation() {
-        mSensorOverlays.show(getSensorId(), BiometricOverlayConstants.REASON_AUTH_KEYGUARD, this);
+        mSensorOverlays.show(getSensorId(), BiometricOverlayConstants.REASON_AUTH_KEYGUARD,
+                this);
 
         try {
             getFreshDaemon().authenticate(0 /* operationId */, getTargetUserId());
@@ -128,8 +132,8 @@
     }
 
     @Override
-    public void onAuthenticated(BiometricAuthenticator.Identifier identifier, boolean authenticated,
-            ArrayList<Byte> hardwareAuthToken) {
+    public void onAuthenticated(BiometricAuthenticator.Identifier identifier,
+            boolean authenticated, ArrayList<Byte> hardwareAuthToken) {
         getLogger().logOnAuthenticated(getContext(), getOperationContext(),
                 authenticated, false /* requireConfirmation */,
                 getTargetUserId(), false /* isBiometricPrompt */);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
index 5d9af53..3371cec 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
@@ -26,6 +26,7 @@
 import android.hardware.fingerprint.Fingerprint;
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.ISidefpsController;
+import android.hardware.fingerprint.IUdfpsOverlay;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -67,12 +68,14 @@
             @NonNull BiometricLogger biometricLogger, @NonNull BiometricContext biometricContext,
             @Nullable IUdfpsOverlayController udfpsOverlayController,
             @Nullable ISidefpsController sidefpsController,
+            @Nullable IUdfpsOverlay udfpsOverlay,
             @FingerprintManager.EnrollReason int enrollReason) {
         super(context, lazyDaemon, token, listener, userId, hardwareAuthToken, owner, utils,
                 timeoutSec, sensorId, true /* shouldVibrate */, biometricLogger,
                 biometricContext);
         setRequestId(requestId);
-        mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
+        mSensorOverlays = new SensorOverlays(udfpsOverlayController,
+                sidefpsController, udfpsOverlay);
 
         mEnrollReason = enrollReason;
         if (enrollReason == FingerprintManager.ENROLL_FIND_SENSOR) {
@@ -102,7 +105,8 @@
 
     @Override
     protected void startHalOperation() {
-        mSensorOverlays.show(getSensorId(), getOverlayReasonFromEnrollReason(mEnrollReason), this);
+        mSensorOverlays.show(getSensorId(), getOverlayReasonFromEnrollReason(mEnrollReason),
+                this);
 
         BiometricNotificationUtils.cancelBadCalibrationNotification(getContext());
         try {
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 6795b6b..45b0f0a6 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -79,6 +79,7 @@
 import android.net.RouteInfo;
 import android.net.UidRangeParcel;
 import android.net.UnderlyingNetworkInfo;
+import android.net.Uri;
 import android.net.VpnManager;
 import android.net.VpnProfileState;
 import android.net.VpnService;
@@ -226,6 +227,16 @@
     private static final int VPN_DEFAULT_SCORE = 101;
 
     /**
+     * The reset session timer for data stall. If a session has not successfully revalidated after
+     * the delay, the session will be torn down and restarted in an attempt to recover. Delay
+     * counter is reset on successful validation only.
+     *
+     * <p>If retries have exceeded the length of this array, the last entry in the array will be
+     * used as a repeating interval.
+     */
+    private static final long[] DATA_STALL_RESET_DELAYS_SEC = {30L, 60L, 120L, 240L, 480L, 960L};
+
+    /**
      * The initial token value of IKE session.
      */
     private static final int STARTING_TOKEN = -1;
@@ -271,6 +282,7 @@
     private final UserManager mUserManager;
 
     private final VpnProfileStore mVpnProfileStore;
+    protected boolean mDataStallSuspected = false;
 
     @VisibleForTesting
     VpnProfileStore getVpnProfileStore() {
@@ -522,10 +534,28 @@
                 @NonNull LinkProperties lp,
                 @NonNull NetworkScore score,
                 @NonNull NetworkAgentConfig config,
-                @Nullable NetworkProvider provider) {
+                @Nullable NetworkProvider provider,
+                @Nullable ValidationStatusCallback callback) {
             return new VpnNetworkAgentWrapper(
-                    context, looper, logTag, nc, lp, score, config, provider);
+                    context, looper, logTag, nc, lp, score, config, provider, callback);
         }
+
+        /**
+         * Get the length of time to wait before resetting the ike session when a data stall is
+         * suspected.
+         */
+        public long getDataStallResetSessionSeconds(int count) {
+            if (count >= DATA_STALL_RESET_DELAYS_SEC.length) {
+                return DATA_STALL_RESET_DELAYS_SEC[DATA_STALL_RESET_DELAYS_SEC.length - 1];
+            } else {
+                return DATA_STALL_RESET_DELAYS_SEC[count];
+            }
+        }
+    }
+
+    @VisibleForTesting
+    interface ValidationStatusCallback {
+        void onValidationStatus(int status);
     }
 
     public Vpn(Looper looper, Context context, INetworkManagementService netService, INetd netd,
@@ -1460,6 +1490,11 @@
 
     @GuardedBy("this")
     private void agentConnect() {
+        agentConnect(null /* validationCallback */);
+    }
+
+    @GuardedBy("this")
+    private void agentConnect(@Nullable ValidationStatusCallback validationCallback) {
         LinkProperties lp = makeLinkProperties();
 
         // VPN either provide a default route (IPv4 or IPv6 or both), or they are a split tunnel
@@ -1507,7 +1542,7 @@
         mNetworkAgent = mDeps.newNetworkAgent(mContext, mLooper, NETWORKTYPE /* logtag */,
                 mNetworkCapabilities, lp,
                 new NetworkScore.Builder().setLegacyInt(VPN_DEFAULT_SCORE).build(),
-                networkAgentConfig, mNetworkProvider);
+                networkAgentConfig, mNetworkProvider, validationCallback);
         final long token = Binder.clearCallingIdentity();
         try {
             mNetworkAgent.register();
@@ -2723,7 +2758,7 @@
 
         @Nullable private ScheduledFuture<?> mScheduledHandleNetworkLostFuture;
         @Nullable private ScheduledFuture<?> mScheduledHandleRetryIkeSessionFuture;
-
+        @Nullable private ScheduledFuture<?> mScheduledHandleDataStallFuture;
         /** Signal to ensure shutdown is honored even if a new Network is connected. */
         private boolean mIsRunning = true;
 
@@ -2750,6 +2785,14 @@
         private boolean mMobikeEnabled = false;
 
         /**
+         * The number of attempts to reset the IKE session since the last successful connection.
+         *
+         * <p>This variable controls the retry delay, and is reset when the VPN pass network
+         * validation.
+         */
+        private int mDataStallRetryCount = 0;
+
+        /**
          * The number of attempts since the last successful connection.
          *
          * <p>This variable controls the retry delay, and is reset when a new IKE session is
@@ -2931,7 +2974,7 @@
                         if (isSettingsVpnLocked()) {
                             prepareStatusIntent();
                         }
-                        agentConnect();
+                        agentConnect(this::onValidationStatus);
                         return; // Link properties are already sent.
                     } else {
                         // Underlying networks also set in agentConnect()
@@ -3200,18 +3243,52 @@
                     // Ignore stale runner.
                     if (mVpnRunner != Vpn.IkeV2VpnRunner.this) return;
 
-                    // Handle the report only for current VPN network.
+                    // Handle the report only for current VPN network. If data stall is already
+                    // reported, ignoring the other reports. It means that the stall is not
+                    // recovered by MOBIKE and should be on the way to reset the ike session.
                     if (mNetworkAgent != null
-                            && mNetworkAgent.getNetwork().equals(report.getNetwork())) {
+                            && mNetworkAgent.getNetwork().equals(report.getNetwork())
+                            && !mDataStallSuspected) {
                         Log.d(TAG, "Data stall suspected");
 
                         // Trigger MOBIKE.
                         maybeMigrateIkeSession(mActiveNetwork);
+                        mDataStallSuspected = true;
                     }
                 }
             }
         }
 
+        public void onValidationStatus(int status) {
+            if (status == NetworkAgent.VALIDATION_STATUS_VALID) {
+                // No data stall now. Reset it.
+                mExecutor.execute(() -> {
+                    mDataStallSuspected = false;
+                    mDataStallRetryCount = 0;
+                    if (mScheduledHandleDataStallFuture != null) {
+                        Log.d(TAG, "Recovered from stall. Cancel pending reset action.");
+                        mScheduledHandleDataStallFuture.cancel(false /* mayInterruptIfRunning */);
+                        mScheduledHandleDataStallFuture = null;
+                    }
+                });
+            } else {
+                // Skip other invalid status if the scheduled recovery exists.
+                if (mScheduledHandleDataStallFuture != null) return;
+
+                mScheduledHandleDataStallFuture = mExecutor.schedule(() -> {
+                    if (mDataStallSuspected) {
+                        Log.d(TAG, "Reset session to recover stalled network");
+                        // This will reset old state if it exists.
+                        startIkeSession(mActiveNetwork);
+                    }
+
+                    // Reset mScheduledHandleDataStallFuture since it's already run on executor
+                    // thread.
+                    mScheduledHandleDataStallFuture = null;
+                }, mDeps.getDataStallResetSessionSeconds(mDataStallRetryCount++), TimeUnit.SECONDS);
+            }
+        }
+
         /**
          * Handles loss of the default underlying network
          *
@@ -4339,6 +4416,7 @@
     // un-finalized.
     @VisibleForTesting
     public static class VpnNetworkAgentWrapper extends NetworkAgent {
+        private final ValidationStatusCallback mCallback;
         /** Create an VpnNetworkAgentWrapper */
         public VpnNetworkAgentWrapper(
                 @NonNull Context context,
@@ -4348,8 +4426,10 @@
                 @NonNull LinkProperties lp,
                 @NonNull NetworkScore score,
                 @NonNull NetworkAgentConfig config,
-                @Nullable NetworkProvider provider) {
+                @Nullable NetworkProvider provider,
+                @Nullable ValidationStatusCallback callback) {
             super(context, looper, logTag, nc, lp, score, config, provider);
+            mCallback = callback;
         }
 
         /** Update the LinkProperties */
@@ -4371,6 +4451,13 @@
         public void onNetworkUnwanted() {
             // We are user controlled, not driven by NetworkRequest.
         }
+
+        @Override
+        public void onValidationStatus(int status, Uri redirectUri) {
+            if (mCallback != null) {
+                mCallback.onValidationStatus(status);
+            }
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 7e80b7d..e907ebf 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -127,6 +127,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.display.BrightnessSynchronizer;
 import com.android.internal.util.DumpUtils;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.AnimationThread;
 import com.android.server.DisplayThread;
@@ -151,6 +152,7 @@
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.Consumer;
 
+
 /**
  * Manages attached displays.
  * <p>
@@ -1900,6 +1902,14 @@
                 if (displayDevice == null) {
                     return;
                 }
+                if (mLogicalDisplayMapper.getDisplayLocked(displayDevice)
+                        .getDisplayInfoLocked().type == Display.TYPE_INTERNAL) {
+                    FrameworkStatsLog.write(FrameworkStatsLog.BRIGHTNESS_CONFIGURATION_UPDATED,
+                                c.getCurve().first,
+                                c.getCurve().second,
+                                // should not be logged for virtual displays
+                                uniqueId);
+                }
                 mPersistentDataStore.setBrightnessConfigurationForDisplayLocked(c, displayDevice,
                         userSerial, packageName);
             } finally {
diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java
index cd9ef09..c3313e0 100644
--- a/services/core/java/com/android/server/dreams/DreamController.java
+++ b/services/core/java/com/android/server/dreams/DreamController.java
@@ -245,6 +245,7 @@
 
                 if (mSentStartBroadcast) {
                     mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL);
+                    mSentStartBroadcast = false;
                 }
 
                 mActivityTaskManager.removeRootTasksWithActivityTypes(
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index 6e2cceb..4ca4817 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -23,12 +23,14 @@
 
 import static com.android.server.wm.ActivityInterceptorCallback.DREAM_MANAGER_ORDERED_ID;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.TaskInfo;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -38,6 +40,8 @@
 import android.content.pm.ServiceInfo;
 import android.database.ContentObserver;
 import android.hardware.display.AmbientDisplayConfiguration;
+import android.net.Uri;
+import android.os.BatteryManager;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Handler;
@@ -72,6 +76,8 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -88,6 +94,15 @@
     private static final String DOZE_WAKE_LOCK_TAG = "dream:doze";
     private static final String DREAM_WAKE_LOCK_TAG = "dream:dream";
 
+    /** Constants for the when to activate dreams. */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({DREAM_ON_DOCK, DREAM_ON_CHARGE, DREAM_ON_DOCK_OR_CHARGE})
+    public @interface WhenToDream {}
+    private static final int DREAM_DISABLED = 0x0;
+    private static final int DREAM_ON_DOCK = 0x1;
+    private static final int DREAM_ON_CHARGE = 0x2;
+    private static final int DREAM_ON_DOCK_OR_CHARGE = 0x3;
+
     private final Object mLock = new Object();
 
     private final Context mContext;
@@ -101,12 +116,20 @@
     private final DreamUiEventLogger mDreamUiEventLogger;
     private final ComponentName mAmbientDisplayComponent;
     private final boolean mDismissDreamOnActivityStart;
+    private final boolean mDreamsOnlyEnabledForSystemUser;
+    private final boolean mDreamsEnabledByDefaultConfig;
+    private final boolean mDreamsActivatedOnChargeByDefault;
+    private final boolean mDreamsActivatedOnDockByDefault;
 
     @GuardedBy("mLock")
     private DreamRecord mCurrentDream;
 
     private boolean mForceAmbientDisplayEnabled;
-    private final boolean mDreamsOnlyEnabledForSystemUser;
+    private SettingsObserver mSettingsObserver;
+    private boolean mDreamsEnabledSetting;
+    @WhenToDream private int mWhenToDream;
+    private boolean mIsDocked;
+    private boolean mIsCharging;
 
     // A temporary dream component that, when present, takes precedence over user configured dream
     // component.
@@ -144,6 +167,37 @@
                 }
             };
 
+    private final BroadcastReceiver mChargingReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mIsCharging = (BatteryManager.ACTION_CHARGING.equals(intent.getAction()));
+        }
+    };
+
+    private final BroadcastReceiver mDockStateReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_DOCK_EVENT.equals(intent.getAction())) {
+                int dockState = intent.getIntExtra(Intent.EXTRA_DOCK_STATE,
+                        Intent.EXTRA_DOCK_STATE_UNDOCKED);
+                mIsDocked = dockState != Intent.EXTRA_DOCK_STATE_UNDOCKED;
+            }
+        }
+    };
+
+    private final class SettingsObserver extends ContentObserver {
+        SettingsObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            synchronized (mLock) {
+                updateWhenToDreamSettings();
+            }
+        }
+    }
+
     public DreamManagerService(Context context) {
         super(context);
         mContext = context;
@@ -164,6 +218,14 @@
                 mContext.getResources().getBoolean(R.bool.config_dreamsOnlyEnabledForSystemUser);
         mDismissDreamOnActivityStart = mContext.getResources().getBoolean(
                 R.bool.config_dismissDreamOnActivityStart);
+
+        mDreamsEnabledByDefaultConfig = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_dreamsEnabledByDefault);
+        mDreamsActivatedOnChargeByDefault = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault);
+        mDreamsActivatedOnDockByDefault = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault);
+        mSettingsObserver = new SettingsObserver(mHandler);
     }
 
     @Override
@@ -197,6 +259,30 @@
                         DREAM_MANAGER_ORDERED_ID,
                         mActivityInterceptorCallback);
             }
+
+            mContext.registerReceiver(
+                    mDockStateReceiver, new IntentFilter(Intent.ACTION_DOCK_EVENT));
+            IntentFilter chargingIntentFilter = new IntentFilter();
+            chargingIntentFilter.addAction(BatteryManager.ACTION_CHARGING);
+            chargingIntentFilter.addAction(BatteryManager.ACTION_DISCHARGING);
+            mContext.registerReceiver(mChargingReceiver, chargingIntentFilter);
+
+            mSettingsObserver = new SettingsObserver(mHandler);
+            mContext.getContentResolver().registerContentObserver(Settings.Secure.getUriFor(
+                            Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP),
+                    false, mSettingsObserver, UserHandle.USER_ALL);
+            mContext.getContentResolver().registerContentObserver(Settings.Secure.getUriFor(
+                            Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK),
+                    false, mSettingsObserver, UserHandle.USER_ALL);
+            mContext.getContentResolver().registerContentObserver(Settings.Secure.getUriFor(
+                            Settings.Secure.SCREENSAVER_ENABLED),
+                    false, mSettingsObserver, UserHandle.USER_ALL);
+
+            // We don't get an initial broadcast for the batter state, so we have to initialize
+            // directly from BatteryManager.
+            mIsCharging = mContext.getSystemService(BatteryManager.class).isCharging();
+
+            updateWhenToDreamSettings();
         }
     }
 
@@ -207,6 +293,14 @@
             pw.println("mCurrentDream=" + mCurrentDream);
             pw.println("mForceAmbientDisplayEnabled=" + mForceAmbientDisplayEnabled);
             pw.println("mDreamsOnlyEnabledForSystemUser=" + mDreamsOnlyEnabledForSystemUser);
+            pw.println("mDreamsEnabledSetting=" + mDreamsEnabledSetting);
+            pw.println("mForceAmbientDisplayEnabled=" + mForceAmbientDisplayEnabled);
+            pw.println("mDreamsOnlyEnabledForSystemUser=" + mDreamsOnlyEnabledForSystemUser);
+            pw.println("mDreamsActivatedOnDockByDefault=" + mDreamsActivatedOnDockByDefault);
+            pw.println("mDreamsActivatedOnChargeByDefault=" + mDreamsActivatedOnChargeByDefault);
+            pw.println("mIsDocked=" + mIsDocked);
+            pw.println("mIsCharging=" + mIsCharging);
+            pw.println("mWhenToDream=" + mWhenToDream);
             pw.println("getDozeComponent()=" + getDozeComponent());
             pw.println();
 
@@ -214,7 +308,28 @@
         }
     }
 
-    /** Whether a real dream is occurring. */
+    private void updateWhenToDreamSettings() {
+        synchronized (mLock) {
+            final ContentResolver resolver = mContext.getContentResolver();
+
+            final int activateWhenCharging = (Settings.Secure.getIntForUser(resolver,
+                    Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
+                    mDreamsActivatedOnChargeByDefault ? 1 : 0,
+                    UserHandle.USER_CURRENT) != 0) ? DREAM_ON_CHARGE : DREAM_DISABLED;
+            final int activateWhenDocked = (Settings.Secure.getIntForUser(resolver,
+                    Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK,
+                    mDreamsActivatedOnDockByDefault ? 1 : 0,
+                    UserHandle.USER_CURRENT) != 0) ? DREAM_ON_DOCK : DREAM_DISABLED;
+            mWhenToDream = activateWhenCharging + activateWhenDocked;
+
+            mDreamsEnabledSetting = (Settings.Secure.getIntForUser(resolver,
+                    Settings.Secure.SCREENSAVER_ENABLED,
+                    mDreamsEnabledByDefaultConfig ? 1 : 0,
+                    UserHandle.USER_CURRENT) != 0);
+        }
+    }
+
+        /** Whether a real dream is occurring. */
     private boolean isDreamingInternal() {
         synchronized (mLock) {
             return mCurrentDream != null && !mCurrentDream.isPreview
@@ -236,6 +351,30 @@
         }
     }
 
+    /** Whether dreaming can start given user settings and the current dock/charge state. */
+    private boolean canStartDreamingInternal(boolean isScreenOn) {
+        synchronized (mLock) {
+            // Can't start dreaming if we are already dreaming.
+            if (isScreenOn && isDreamingInternal()) {
+                return false;
+            }
+
+            if (!mDreamsEnabledSetting) {
+                return false;
+            }
+
+            if ((mWhenToDream & DREAM_ON_CHARGE) == DREAM_ON_CHARGE) {
+                return mIsCharging;
+            }
+
+            if ((mWhenToDream & DREAM_ON_DOCK) == DREAM_ON_DOCK) {
+                return mIsDocked;
+            }
+
+            return false;
+        }
+    }
+
     protected void requestStartDreamFromShell() {
         requestDreamInternal();
     }
@@ -869,6 +1008,11 @@
         }
 
         @Override
+        public boolean canStartDreaming(boolean isScreenOn) {
+            return canStartDreamingInternal(isScreenOn);
+        }
+
+        @Override
         public ComponentName getActiveDreamComponent(boolean doze) {
             return getActiveDreamComponentInternal(doze);
         }
diff --git a/services/core/java/com/android/server/infra/FrameworkResourcesServiceNameResolver.java b/services/core/java/com/android/server/infra/FrameworkResourcesServiceNameResolver.java
index 5253d34..d4e8f27 100644
--- a/services/core/java/com/android/server/infra/FrameworkResourcesServiceNameResolver.java
+++ b/services/core/java/com/android/server/infra/FrameworkResourcesServiceNameResolver.java
@@ -19,28 +19,9 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.StringRes;
-import android.annotation.UserIdInt;
-import android.app.AppGlobals;
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.pm.ServiceInfo;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.SystemClock;
-import android.text.TextUtils;
-import android.util.Slog;
-import android.util.SparseArray;
-import android.util.SparseBooleanArray;
-import android.util.TimeUtils;
-
-import com.android.internal.annotations.GuardedBy;
 
 import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
 
 /**
  * Gets the service name using a framework resources, temporarily changing the service if necessary
@@ -48,259 +29,42 @@
  *
  * @hide
  */
-public final class FrameworkResourcesServiceNameResolver implements ServiceNameResolver {
+public final class FrameworkResourcesServiceNameResolver extends ServiceNameBaseResolver {
 
-    private static final String TAG = FrameworkResourcesServiceNameResolver.class.getSimpleName();
-
-    /** Handler message to {@link #resetTemporaryService(int)} */
-    private static final int MSG_RESET_TEMPORARY_SERVICE = 0;
-
-    @NonNull
-    private final Context mContext;
-    @NonNull
-    private final Object mLock = new Object();
-    @StringRes
     private final int mStringResourceId;
     @ArrayRes
     private final int mArrayResourceId;
-    private final boolean mIsMultiple;
-    /**
-     * Map of temporary service name list set by {@link #setTemporaryServices(int, String[], int)},
-     * keyed by {@code userId}.
-     *
-     * <p>Typically used by Shell command and/or CTS tests to configure temporary services if
-     * mIsMultiple is true.
-     */
-    @GuardedBy("mLock")
-    private final SparseArray<String[]> mTemporaryServiceNamesList = new SparseArray<>();
-    /**
-     * Map of default services that have been disabled by
-     * {@link #setDefaultServiceEnabled(int, boolean)},keyed by {@code userId}.
-     *
-     * <p>Typically used by Shell command and/or CTS tests.
-     */
-    @GuardedBy("mLock")
-    private final SparseBooleanArray mDefaultServicesDisabled = new SparseBooleanArray();
-    @Nullable
-    private NameResolverListener mOnSetCallback;
-    /**
-     * When the temporary service will expire (and reset back to the default).
-     */
-    @GuardedBy("mLock")
-    private long mTemporaryServiceExpiration;
-
-    /**
-     * Handler used to reset the temporary service name.
-     */
-    @GuardedBy("mLock")
-    private Handler mTemporaryHandler;
 
     public FrameworkResourcesServiceNameResolver(@NonNull Context context,
             @StringRes int resourceId) {
-        mContext = context;
+        super(context, false);
         mStringResourceId = resourceId;
         mArrayResourceId = -1;
-        mIsMultiple = false;
     }
 
     public FrameworkResourcesServiceNameResolver(@NonNull Context context,
             @ArrayRes int resourceId, boolean isMultiple) {
+        super(context, isMultiple);
         if (!isMultiple) {
             throw new UnsupportedOperationException("Please use "
                     + "FrameworkResourcesServiceNameResolver(context, @StringRes int) constructor "
                     + "if single service mode is requested.");
         }
-        mContext = context;
         mStringResourceId = -1;
         mArrayResourceId = resourceId;
-        mIsMultiple = true;
     }
 
     @Override
-    public void setOnTemporaryServiceNameChangedCallback(@NonNull NameResolverListener callback) {
-        synchronized (mLock) {
-            this.mOnSetCallback = callback;
-        }
+    public String[] readServiceNameList(int userId) {
+        return mContext.getResources().getStringArray(mArrayResourceId);
     }
 
+    @Nullable
     @Override
-    public String getServiceName(@UserIdInt int userId) {
-        String[] serviceNames = getServiceNameList(userId);
-        return (serviceNames == null || serviceNames.length == 0) ? null : serviceNames[0];
+    public String readServiceName(int userId) {
+        return mContext.getResources().getString(mStringResourceId);
     }
 
-    @Override
-    public String getDefaultServiceName(@UserIdInt int userId) {
-        String[] serviceNames = getDefaultServiceNameList(userId);
-        return (serviceNames == null || serviceNames.length == 0) ? null : serviceNames[0];
-    }
-
-    /**
-     * Gets the default list of the service names for the given user.
-     *
-     * <p>Typically implemented by services which want to provide multiple backends.
-     */
-    @Override
-    public String[] getServiceNameList(int userId) {
-        synchronized (mLock) {
-            String[] temporaryNames = mTemporaryServiceNamesList.get(userId);
-            if (temporaryNames != null) {
-                // Always log it, as it should only be used on CTS or during development
-                Slog.w(TAG, "getServiceName(): using temporary name "
-                        + Arrays.toString(temporaryNames) + " for user " + userId);
-                return temporaryNames;
-            }
-            final boolean disabled = mDefaultServicesDisabled.get(userId);
-            if (disabled) {
-                // Always log it, as it should only be used on CTS or during development
-                Slog.w(TAG, "getServiceName(): temporary name not set and default disabled for "
-                        + "user " + userId);
-                return null;
-            }
-            return getDefaultServiceNameList(userId);
-
-        }
-    }
-
-    /**
-     * Gets the default list of the service names for the given user.
-     *
-     * <p>Typically implemented by services which want to provide multiple backends.
-     */
-    @Override
-    public String[] getDefaultServiceNameList(int userId) {
-        synchronized (mLock) {
-            if (mIsMultiple) {
-                String[] serviceNameList = mContext.getResources().getStringArray(mArrayResourceId);
-                // Filter out unimplemented services
-                // Initialize the validated array as null because we do not know the final size.
-                List<String> validatedServiceNameList = new ArrayList<>();
-                try {
-                    for (int i = 0; i < serviceNameList.length; i++) {
-                        if (TextUtils.isEmpty(serviceNameList[i])) {
-                            continue;
-                        }
-                        ComponentName serviceComponent = ComponentName.unflattenFromString(
-                                serviceNameList[i]);
-                        ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo(
-                                serviceComponent,
-                                PackageManager.MATCH_DIRECT_BOOT_AWARE
-                                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId);
-                        if (serviceInfo != null) {
-                            validatedServiceNameList.add(serviceNameList[i]);
-                        }
-                    }
-                } catch (Exception e) {
-                    Slog.e(TAG, "Could not validate provided services.", e);
-                }
-                String[] validatedServiceNameArray = new String[validatedServiceNameList.size()];
-                return validatedServiceNameList.toArray(validatedServiceNameArray);
-            } else {
-                final String name = mContext.getString(mStringResourceId);
-                return TextUtils.isEmpty(name) ? new String[0] : new String[]{name};
-            }
-        }
-    }
-
-    @Override
-    public boolean isConfiguredInMultipleMode() {
-        return mIsMultiple;
-    }
-
-    @Override
-    public boolean isTemporary(@UserIdInt int userId) {
-        synchronized (mLock) {
-            return mTemporaryServiceNamesList.get(userId) != null;
-        }
-    }
-
-    @Override
-    public void setTemporaryService(@UserIdInt int userId, @NonNull String componentName,
-            int durationMs) {
-        setTemporaryServices(userId, new String[]{componentName}, durationMs);
-    }
-
-    @Override
-    public void setTemporaryServices(int userId, @NonNull String[] componentNames, int durationMs) {
-        synchronized (mLock) {
-            mTemporaryServiceNamesList.put(userId, componentNames);
-
-            if (mTemporaryHandler == null) {
-                mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) {
-                    @Override
-                    public void handleMessage(Message msg) {
-                        if (msg.what == MSG_RESET_TEMPORARY_SERVICE) {
-                            synchronized (mLock) {
-                                resetTemporaryService(userId);
-                            }
-                        } else {
-                            Slog.wtf(TAG, "invalid handler msg: " + msg);
-                        }
-                    }
-                };
-            } else {
-                mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE);
-            }
-            mTemporaryServiceExpiration = SystemClock.elapsedRealtime() + durationMs;
-            mTemporaryHandler.sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_SERVICE, durationMs);
-            for (int i = 0; i < componentNames.length; i++) {
-                notifyTemporaryServiceNameChangedLocked(userId, componentNames[i],
-                        /* isTemporary= */ true);
-            }
-        }
-    }
-
-    @Override
-    public void resetTemporaryService(@UserIdInt int userId) {
-        synchronized (mLock) {
-            Slog.i(TAG, "resetting temporary service for user " + userId + " from "
-                    + Arrays.toString(mTemporaryServiceNamesList.get(userId)));
-            mTemporaryServiceNamesList.remove(userId);
-            if (mTemporaryHandler != null) {
-                mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE);
-                mTemporaryHandler = null;
-            }
-            notifyTemporaryServiceNameChangedLocked(userId, /* newTemporaryName= */ null,
-                    /* isTemporary= */ false);
-        }
-    }
-
-    @Override
-    public boolean setDefaultServiceEnabled(int userId, boolean enabled) {
-        synchronized (mLock) {
-            final boolean currentlyEnabled = isDefaultServiceEnabledLocked(userId);
-            if (currentlyEnabled == enabled) {
-                Slog.i(TAG, "setDefaultServiceEnabled(" + userId + "): already " + enabled);
-                return false;
-            }
-            if (enabled) {
-                Slog.i(TAG, "disabling default service for user " + userId);
-                mDefaultServicesDisabled.removeAt(userId);
-            } else {
-                Slog.i(TAG, "enabling default service for user " + userId);
-                mDefaultServicesDisabled.put(userId, true);
-            }
-        }
-        return true;
-    }
-
-    @Override
-    public boolean isDefaultServiceEnabled(int userId) {
-        synchronized (mLock) {
-            return isDefaultServiceEnabledLocked(userId);
-        }
-    }
-
-    private boolean isDefaultServiceEnabledLocked(int userId) {
-        return !mDefaultServicesDisabled.get(userId);
-    }
-
-    @Override
-    public String toString() {
-        synchronized (mLock) {
-            return "FrameworkResourcesServiceNamer[temps=" + mTemporaryServiceNamesList + "]";
-        }
-    }
 
     // TODO(b/117779333): support proto
     @Override
@@ -314,31 +78,4 @@
             pw.print(mDefaultServicesDisabled.size());
         }
     }
-
-    // TODO(b/117779333): support proto
-    @Override
-    public void dumpShort(@NonNull PrintWriter pw, @UserIdInt int userId) {
-        synchronized (mLock) {
-            final String[] temporaryNames = mTemporaryServiceNamesList.get(userId);
-            if (temporaryNames != null) {
-                pw.print("tmpName=");
-                pw.print(Arrays.toString(temporaryNames));
-                final long ttl = mTemporaryServiceExpiration - SystemClock.elapsedRealtime();
-                pw.print(" (expires in ");
-                TimeUtils.formatDuration(ttl, pw);
-                pw.print("), ");
-            }
-            pw.print("defaultName=");
-            pw.print(getDefaultServiceName(userId));
-            final boolean disabled = mDefaultServicesDisabled.get(userId);
-            pw.println(disabled ? " (disabled)" : " (enabled)");
-        }
-    }
-
-    private void notifyTemporaryServiceNameChangedLocked(@UserIdInt int userId,
-            @Nullable String newTemporaryName, boolean isTemporary) {
-        if (mOnSetCallback != null) {
-            mOnSetCallback.onNameResolved(userId, newTemporaryName, isTemporary);
-        }
-    }
 }
diff --git a/services/core/java/com/android/server/infra/SecureSettingsServiceNameResolver.java b/services/core/java/com/android/server/infra/SecureSettingsServiceNameResolver.java
index cac7f53..17d75e6 100644
--- a/services/core/java/com/android/server/infra/SecureSettingsServiceNameResolver.java
+++ b/services/core/java/com/android/server/infra/SecureSettingsServiceNameResolver.java
@@ -19,8 +19,11 @@
 import android.annotation.UserIdInt;
 import android.content.Context;
 import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.ArraySet;
 
 import java.io.PrintWriter;
+import java.util.Set;
 
 /**
  * Gets the service name using a property from the {@link android.provider.Settings.Secure}
@@ -28,21 +31,34 @@
  *
  * @hide
  */
-public final class SecureSettingsServiceNameResolver implements ServiceNameResolver {
+public final class SecureSettingsServiceNameResolver extends ServiceNameBaseResolver {
+    /**
+     * The delimiter to be used to parse the secure settings string. Services must make sure
+     * that this delimiter is used while adding component names to their secure setting property.
+     */
+    private static final char COMPONENT_NAME_SEPARATOR = ':';
 
-    private final @NonNull Context mContext;
+    private final TextUtils.SimpleStringSplitter mStringColonSplitter =
+            new TextUtils.SimpleStringSplitter(COMPONENT_NAME_SEPARATOR);
 
     @NonNull
     private final String mProperty;
 
     public SecureSettingsServiceNameResolver(@NonNull Context context, @NonNull String property) {
-        mContext = context;
-        mProperty = property;
+        this(context, property, /*isMultiple*/false);
     }
 
-    @Override
-    public String getDefaultServiceName(@UserIdInt int userId) {
-        return Settings.Secure.getStringForUser(mContext.getContentResolver(), mProperty, userId);
+    /**
+     *
+     * @param context the context required to retrieve the secure setting value
+     * @param property name of the secure setting key
+     * @param isMultiple true if the system service using this resolver needs to connect to
+     *                   multiple remote services, false otherwise
+     */
+    public SecureSettingsServiceNameResolver(@NonNull Context context, @NonNull String property,
+            boolean isMultiple) {
+        super(context, isMultiple);
+        mProperty = property;
     }
 
     // TODO(b/117779333): support proto
@@ -61,4 +77,34 @@
     public String toString() {
         return "SecureSettingsServiceNameResolver[" + mProperty + "]";
     }
+
+    @Override
+    public String[] readServiceNameList(int userId) {
+        return parseColonDelimitedServiceNames(
+                Settings.Secure.getStringForUser(
+                        mContext.getContentResolver(), mProperty, userId));
+    }
+
+    @Override
+    public String readServiceName(int userId) {
+        return Settings.Secure.getStringForUser(
+                mContext.getContentResolver(), mProperty, userId);
+    }
+
+    private String[] parseColonDelimitedServiceNames(String serviceNames) {
+        final Set<String> delimitedServices = new ArraySet<>();
+        if (!TextUtils.isEmpty(serviceNames)) {
+            final TextUtils.SimpleStringSplitter splitter = mStringColonSplitter;
+            splitter.setString(serviceNames);
+            while (splitter.hasNext()) {
+                final String str = splitter.next();
+                if (TextUtils.isEmpty(str)) {
+                    continue;
+                }
+                delimitedServices.add(str);
+            }
+        }
+        String[] delimitedServicesArray = new String[delimitedServices.size()];
+        return delimitedServices.toArray(delimitedServicesArray);
+    }
 }
diff --git a/services/core/java/com/android/server/infra/ServiceNameBaseResolver.java b/services/core/java/com/android/server/infra/ServiceNameBaseResolver.java
new file mode 100644
index 0000000..76ea05e
--- /dev/null
+++ b/services/core/java/com/android/server/infra/ServiceNameBaseResolver.java
@@ -0,0 +1,325 @@
+/*
+ * 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.server.infra;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.AppGlobals;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Gets the service name using a framework resources, temporarily changing the service if necessary
+ * (typically during CTS tests or service development).
+ *
+ * @hide
+ */
+public abstract class ServiceNameBaseResolver implements ServiceNameResolver {
+
+    private static final String TAG = ServiceNameBaseResolver.class.getSimpleName();
+
+    /** Handler message to {@link #resetTemporaryService(int)} */
+    private static final int MSG_RESET_TEMPORARY_SERVICE = 0;
+
+    @NonNull
+    protected final Context mContext;
+    @NonNull
+    protected final Object mLock = new Object();
+
+    protected final boolean mIsMultiple;
+    /**
+     * Map of temporary service name list set by {@link #setTemporaryServices(int, String[], int)},
+     * keyed by {@code userId}.
+     *
+     * <p>Typically used by Shell command and/or CTS tests to configure temporary services if
+     * mIsMultiple is true.
+     */
+    @GuardedBy("mLock")
+    protected final SparseArray<String[]> mTemporaryServiceNamesList = new SparseArray<>();
+    /**
+     * Map of default services that have been disabled by
+     * {@link #setDefaultServiceEnabled(int, boolean)},keyed by {@code userId}.
+     *
+     * <p>Typically used by Shell command and/or CTS tests.
+     */
+    @GuardedBy("mLock")
+    protected final SparseBooleanArray mDefaultServicesDisabled = new SparseBooleanArray();
+    @Nullable
+    private NameResolverListener mOnSetCallback;
+    /**
+     * When the temporary service will expire (and reset back to the default).
+     */
+    @GuardedBy("mLock")
+    private long mTemporaryServiceExpiration;
+
+    /**
+     * Handler used to reset the temporary service name.
+     */
+    @GuardedBy("mLock")
+    private Handler mTemporaryHandler;
+
+    protected ServiceNameBaseResolver(Context context, boolean isMultiple) {
+        mContext = context;
+        mIsMultiple = isMultiple;
+    }
+
+    @Override
+    public void setOnTemporaryServiceNameChangedCallback(@NonNull NameResolverListener callback) {
+        synchronized (mLock) {
+            this.mOnSetCallback = callback;
+        }
+    }
+
+    @Override
+    public String getServiceName(@UserIdInt int userId) {
+        String[] serviceNames = getServiceNameList(userId);
+        return (serviceNames == null || serviceNames.length == 0) ? null : serviceNames[0];
+    }
+
+    @Override
+    public String getDefaultServiceName(@UserIdInt int userId) {
+        String[] serviceNames = getDefaultServiceNameList(userId);
+        return (serviceNames == null || serviceNames.length == 0) ? null : serviceNames[0];
+    }
+
+    /**
+     * Gets the default list of the service names for the given user.
+     *
+     * <p>Typically implemented by services which want to provide multiple backends.
+     */
+    @Override
+    public String[] getServiceNameList(int userId) {
+        synchronized (mLock) {
+            String[] temporaryNames = mTemporaryServiceNamesList.get(userId);
+            if (temporaryNames != null) {
+                // Always log it, as it should only be used on CTS or during development
+                Slog.w(TAG, "getServiceName(): using temporary name "
+                        + Arrays.toString(temporaryNames) + " for user " + userId);
+                return temporaryNames;
+            }
+            final boolean disabled = mDefaultServicesDisabled.get(userId);
+            if (disabled) {
+                // Always log it, as it should only be used on CTS or during development
+                Slog.w(TAG, "getServiceName(): temporary name not set and default disabled for "
+                        + "user " + userId);
+                return null;
+            }
+            return getDefaultServiceNameList(userId);
+
+        }
+    }
+
+    /**
+     * Base classes must override this to read from the desired config e.g. framework resource,
+     * secure settings etc.
+     */
+    @Nullable
+    public abstract String[] readServiceNameList(int userId);
+
+    /**
+     * Base classes must override this to read from the desired config e.g. framework resource,
+     * secure settings etc.
+     */
+    @Nullable
+    public abstract String readServiceName(int userId);
+
+    /**
+     * Gets the default list of the service names for the given user.
+     *
+     * <p>Typically implemented by services which want to provide multiple backends.
+     */
+    @Override
+    public String[] getDefaultServiceNameList(int userId) {
+        synchronized (mLock) {
+            if (mIsMultiple) {
+                String[] serviceNameList = readServiceNameList(userId);
+                // Filter out unimplemented services
+                // Initialize the validated array as null because we do not know the final size.
+                List<String> validatedServiceNameList = new ArrayList<>();
+                try {
+                    for (int i = 0; i < serviceNameList.length; i++) {
+                        if (TextUtils.isEmpty(serviceNameList[i])) {
+                            continue;
+                        }
+                        ComponentName serviceComponent = ComponentName.unflattenFromString(
+                                serviceNameList[i]);
+                        ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo(
+                                serviceComponent,
+                                PackageManager.MATCH_DIRECT_BOOT_AWARE
+                                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId);
+                        if (serviceInfo != null) {
+                            validatedServiceNameList.add(serviceNameList[i]);
+                        }
+                    }
+                } catch (Exception e) {
+                    Slog.e(TAG, "Could not validate provided services.", e);
+                }
+                String[] validatedServiceNameArray = new String[validatedServiceNameList.size()];
+                return validatedServiceNameList.toArray(validatedServiceNameArray);
+            } else {
+                final String name = readServiceName(userId);
+                return TextUtils.isEmpty(name) ? new String[0] : new String[]{name};
+            }
+        }
+    }
+
+    @Override
+    public boolean isConfiguredInMultipleMode() {
+        return mIsMultiple;
+    }
+
+    @Override
+    public boolean isTemporary(@UserIdInt int userId) {
+        synchronized (mLock) {
+            return mTemporaryServiceNamesList.get(userId) != null;
+        }
+    }
+
+    @Override
+    public void setTemporaryService(@UserIdInt int userId, @NonNull String componentName,
+            int durationMs) {
+        setTemporaryServices(userId, new String[]{componentName}, durationMs);
+    }
+
+    @Override
+    public void setTemporaryServices(int userId, @NonNull String[] componentNames, int durationMs) {
+        synchronized (mLock) {
+            mTemporaryServiceNamesList.put(userId, componentNames);
+
+            if (mTemporaryHandler == null) {
+                mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) {
+                    @Override
+                    public void handleMessage(Message msg) {
+                        if (msg.what == MSG_RESET_TEMPORARY_SERVICE) {
+                            synchronized (mLock) {
+                                resetTemporaryService(userId);
+                            }
+                        } else {
+                            Slog.wtf(TAG, "invalid handler msg: " + msg);
+                        }
+                    }
+                };
+            } else {
+                mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE);
+            }
+            mTemporaryServiceExpiration = SystemClock.elapsedRealtime() + durationMs;
+            mTemporaryHandler.sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_SERVICE, durationMs);
+            for (int i = 0; i < componentNames.length; i++) {
+                notifyTemporaryServiceNameChangedLocked(userId, componentNames[i],
+                        /* isTemporary= */ true);
+            }
+        }
+    }
+
+    @Override
+    public void resetTemporaryService(@UserIdInt int userId) {
+        synchronized (mLock) {
+            Slog.i(TAG, "resetting temporary service for user " + userId + " from "
+                    + Arrays.toString(mTemporaryServiceNamesList.get(userId)));
+            mTemporaryServiceNamesList.remove(userId);
+            if (mTemporaryHandler != null) {
+                mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE);
+                mTemporaryHandler = null;
+            }
+            notifyTemporaryServiceNameChangedLocked(userId, /* newTemporaryName= */ null,
+                    /* isTemporary= */ false);
+        }
+    }
+
+    @Override
+    public boolean setDefaultServiceEnabled(int userId, boolean enabled) {
+        synchronized (mLock) {
+            final boolean currentlyEnabled = isDefaultServiceEnabledLocked(userId);
+            if (currentlyEnabled == enabled) {
+                Slog.i(TAG, "setDefaultServiceEnabled(" + userId + "): already " + enabled);
+                return false;
+            }
+            if (enabled) {
+                Slog.i(TAG, "disabling default service for user " + userId);
+                mDefaultServicesDisabled.removeAt(userId);
+            } else {
+                Slog.i(TAG, "enabling default service for user " + userId);
+                mDefaultServicesDisabled.put(userId, true);
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean isDefaultServiceEnabled(int userId) {
+        synchronized (mLock) {
+            return isDefaultServiceEnabledLocked(userId);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private boolean isDefaultServiceEnabledLocked(int userId) {
+        return !mDefaultServicesDisabled.get(userId);
+    }
+
+    @Override
+    public String toString() {
+        synchronized (mLock) {
+            return "FrameworkResourcesServiceNamer[temps=" + mTemporaryServiceNamesList + "]";
+        }
+    }
+
+    // TODO(b/117779333): support proto
+    @Override
+    public void dumpShort(@NonNull PrintWriter pw, @UserIdInt int userId) {
+        synchronized (mLock) {
+            final String[] temporaryNames = mTemporaryServiceNamesList.get(userId);
+            if (temporaryNames != null) {
+                pw.print("tmpName=");
+                pw.print(Arrays.toString(temporaryNames));
+                final long ttl = mTemporaryServiceExpiration - SystemClock.elapsedRealtime();
+                pw.print(" (expires in ");
+                TimeUtils.formatDuration(ttl, pw);
+                pw.print("), ");
+            }
+            pw.print("defaultName=");
+            pw.print(getDefaultServiceName(userId));
+            final boolean disabled = mDefaultServicesDisabled.get(userId);
+            pw.println(disabled ? " (disabled)" : " (enabled)");
+        }
+    }
+
+    private void notifyTemporaryServiceNameChangedLocked(@UserIdInt int userId,
+            @Nullable String newTemporaryName, boolean isTemporary) {
+        if (mOnSetCallback != null) {
+            mOnSetCallback.onNameResolved(userId, newTemporaryName, isTemporary);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java
index 36199de..9d4f181 100644
--- a/services/core/java/com/android/server/input/BatteryController.java
+++ b/services/core/java/com/android/server/input/BatteryController.java
@@ -371,6 +371,17 @@
         }
     }
 
+    public void notifyStylusGestureStarted(int deviceId, long eventTime) {
+        synchronized (mLock) {
+            final DeviceMonitor monitor = mDeviceMonitors.get(deviceId);
+            if (monitor == null) {
+                return;
+            }
+
+            monitor.onStylusGestureStarted(eventTime);
+        }
+    }
+
     public void dump(PrintWriter pw, String prefix) {
         synchronized (mLock) {
             final String indent = prefix + "  ";
@@ -557,6 +568,8 @@
 
         public void onTimeout(long eventTime) {}
 
+        public void onStylusGestureStarted(long eventTime) {}
+
         // Returns the current battery state that can be used to notify listeners BatteryController.
         public State getBatteryStateForReporting() {
             return new State(mState);
@@ -600,6 +613,22 @@
         }
 
         @Override
+        public void onStylusGestureStarted(long eventTime) {
+            processChangesAndNotify(eventTime, (time) -> {
+                final boolean wasValid = mValidityTimeoutCallback != null;
+                if (!wasValid && mState.capacity == 0.f) {
+                    // Handle a special case where the USI device reports a battery capacity of 0
+                    // at boot until the first battery update. To avoid wrongly sending out a
+                    // battery capacity of 0 if we detect stylus presence before the capacity
+                    // is first updated, do not validate the battery state when the state is not
+                    // valid and the capacity is 0.
+                    return;
+                }
+                markUsiBatteryValid();
+            });
+        }
+
+        @Override
         public void onTimeout(long eventTime) {
             processChangesAndNotify(eventTime, (time) -> markUsiBatteryInvalid());
         }
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 69b0e65..31f63d8 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -19,6 +19,8 @@
 import static android.provider.DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT;
 import static android.view.KeyEvent.KEYCODE_UNKNOWN;
 
+import android.Manifest;
+import android.annotation.EnforcePermission;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManagerInternal;
@@ -2671,6 +2673,12 @@
         mBatteryController.unregisterBatteryListener(deviceId, listener, Binder.getCallingPid());
     }
 
+    @EnforcePermission(Manifest.permission.BLUETOOTH)
+    @Override
+    public String getInputDeviceBluetoothAddress(int deviceId) {
+        return mNative.getBluetoothAddress(deviceId);
+    }
+
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
@@ -3052,6 +3060,12 @@
                 com.android.internal.R.bool.config_perDisplayFocusEnabled);
     }
 
+    // Native callback.
+    @SuppressWarnings("unused")
+    private void notifyStylusGestureStarted(int deviceId, long eventTime) {
+        mBatteryController.notifyStylusGestureStarted(deviceId, eventTime);
+    }
+
     /**
      * Flatten a map into a string list, with value positioned directly next to the
      * key.
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 63c0a88..cfa7fb1 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -204,6 +204,9 @@
     /** Set the displayId on which the mouse cursor should be shown. */
     void setPointerDisplayId(int displayId);
 
+    /** Get the bluetooth address of an input device if known, otherwise return null. */
+    String getBluetoothAddress(int deviceId);
+
     /** The native implementation of InputManagerService methods. */
     class NativeImpl implements NativeInputManagerService {
         /** Pointer to native input manager service object, used by native code. */
@@ -418,5 +421,8 @@
 
         @Override
         public native void setPointerDisplayId(int displayId);
+
+        @Override
+        public native String getBluetoothAddress(int deviceId);
     }
 }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 76495b1..9cb8f43 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -4601,12 +4601,6 @@
             }
             if (!setVisible) {
                 if (mCurClient != null) {
-                    // IMMS only knows of focused window, not the actual IME target.
-                    // e.g. it isn't aware of any window that has both
-                    // NOT_FOCUSABLE, ALT_FOCUSABLE_IM flags set and can the IME target.
-                    // Send it to window manager to hide IME from IME target window.
-                    // TODO(b/139861270): send to mCurClient.client once IMMS is aware of
-                    // actual IME target.
                     mWindowManagerInternal.hideIme(
                             mHideRequestWindowMap.get(windowToken),
                             mCurClient.mSelfReportedDisplayId);
diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java
index 364f6db..4ce0320 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerService.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerService.java
@@ -364,16 +364,19 @@
         // 1.) A normal, non-privileged app querying its own locale.
         // 2.) The installer of the given app querying locales of a package installed by said
         // installer.
-        // 3.) The current input method querying locales of another package.
+        // 3.) The current input method querying locales of the current foreground app.
         // 4.) A privileged system service querying locales of another package.
         // The least privileged case is a normal app performing a query, so check that first and get
         // locales if the package name is owned by the app. Next check if the calling app is the
         // installer of the given app and get locales. Finally check if the calling app is the
-        // current input method. If neither conditions matched, check if the caller has the
-        // necessary permission and fetch locales.
+        // current input method, and that app is querying locales of the current foreground app. If
+        // neither conditions matched, check if the caller has the necessary permission and fetch
+        // locales.
         if (!isPackageOwnedByCaller(appPackageName, userId)
                 && !isCallerInstaller(appPackageName, userId)
-                && !isCallerFromCurrentInputMethod(userId)) {
+                && !(isCallerFromCurrentInputMethod(userId)
+                    && mActivityManagerInternal.isAppForeground(
+                            getPackageUid(appPackageName, userId)))) {
             enforceReadAppSpecificLocalesPermission();
         }
         final long token = Binder.clearCallingIdentity();
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index f653b93..439e9bd 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -132,7 +132,7 @@
                 }
             }
 
-            mEventLogger.log(new EventLogger.StringEvent("mScreenOnOffReceiver", null));
+            mEventLogger.enqueue(new EventLogger.StringEvent("mScreenOnOffReceiver", null));
         }
     };
 
@@ -634,7 +634,7 @@
     /* package */ void updateRunningUserAndProfiles(int newActiveUserId) {
         synchronized (mLock) {
             if (mCurrentActiveUserId != newActiveUserId) {
-                mEventLogger.log(
+                mEventLogger.enqueue(
                         EventLogger.StringEvent.from("switchUser",
                                 "userId: %d", newActiveUserId));
 
@@ -705,7 +705,7 @@
                 obtainMessage(UserHandler::notifyRouterRegistered,
                         userRecord.mHandler, routerRecord));
 
-        mEventLogger.log(EventLogger.StringEvent.from("registerRouter2",
+        mEventLogger.enqueue(EventLogger.StringEvent.from("registerRouter2",
                 "package: %s, uid: %d, pid: %d, router id: %d",
                 packageName, uid, pid, routerRecord.mRouterId));
     }
@@ -718,7 +718,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from(
                         "unregisterRouter2",
                         "package: %s, router id: %d",
@@ -744,7 +744,7 @@
             return;
         }
 
-        mEventLogger.log(EventLogger.StringEvent.from(
+        mEventLogger.enqueue(EventLogger.StringEvent.from(
                 "setDiscoveryRequestWithRouter2",
                 "router id: %d, discovery request: %s",
                 routerRecord.mRouterId, discoveryRequest.toString()));
@@ -766,7 +766,7 @@
         RouterRecord routerRecord = mAllRouterRecords.get(binder);
 
         if (routerRecord != null) {
-            mEventLogger.log(EventLogger.StringEvent.from(
+            mEventLogger.enqueue(EventLogger.StringEvent.from(
                     "setRouteVolumeWithRouter2",
                     "router id: %d, volume: %d",
                     routerRecord.mRouterId, volume));
@@ -851,7 +851,7 @@
             return;
         }
 
-        mEventLogger.log(EventLogger.StringEvent.from(
+        mEventLogger.enqueue(EventLogger.StringEvent.from(
                 "selectRouteWithRouter2",
                 "router id: %d, route: %s",
                 routerRecord.mRouterId, route.getId()));
@@ -871,7 +871,7 @@
             return;
         }
 
-        mEventLogger.log(EventLogger.StringEvent.from(
+        mEventLogger.enqueue(EventLogger.StringEvent.from(
                 "deselectRouteWithRouter2",
                 "router id: %d, route: %s",
                 routerRecord.mRouterId, route.getId()));
@@ -891,7 +891,7 @@
             return;
         }
 
-        mEventLogger.log(EventLogger.StringEvent.from(
+        mEventLogger.enqueue(EventLogger.StringEvent.from(
                 "transferToRouteWithRouter2",
                 "router id: %d, route: %s",
                 routerRecord.mRouterId, route.getId()));
@@ -921,7 +921,7 @@
             return;
         }
 
-        mEventLogger.log(EventLogger.StringEvent.from(
+        mEventLogger.enqueue(EventLogger.StringEvent.from(
                 "setSessionVolumeWithRouter2",
                 "router id: %d, session: %s, volume: %d",
                 routerRecord.mRouterId,  uniqueSessionId, volume));
@@ -941,7 +941,7 @@
             return;
         }
 
-        mEventLogger.log(EventLogger.StringEvent.from(
+        mEventLogger.enqueue(EventLogger.StringEvent.from(
                 "releaseSessionWithRouter2",
                 "router id: %d, session: %s",
                 routerRecord.mRouterId,  uniqueSessionId));
@@ -983,7 +983,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("registerManager",
                         "uid: %d, pid: %d, package: %s, userId: %d",
                         uid, pid, packageName, userId));
@@ -1025,7 +1025,7 @@
         }
         UserRecord userRecord = managerRecord.mUserRecord;
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from(
                         "unregisterManager",
                         "package: %s, userId: %d, managerId: %d",
@@ -1045,7 +1045,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("startScan",
                         "manager: %d", managerRecord.mManagerId));
 
@@ -1059,7 +1059,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("stopScan",
                         "manager: %d", managerRecord.mManagerId));
 
@@ -1076,7 +1076,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("setRouteVolumeWithManager",
                         "managerId: %d, routeId: %s, volume: %d",
                         managerRecord.mManagerId, route.getId(), volume));
@@ -1096,7 +1096,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("requestCreateSessionWithManager",
                         "managerId: %d, routeId: %s",
                         managerRecord.mManagerId, route.getId()));
@@ -1146,7 +1146,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("selectRouteWithManager",
                         "managerId: %d, session: %s, routeId: %s",
                         managerRecord.mManagerId, uniqueSessionId, route.getId()));
@@ -1172,7 +1172,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("deselectRouteWithManager",
                         "managerId: %d, session: %s, routeId: %s",
                         managerRecord.mManagerId, uniqueSessionId, route.getId()));
@@ -1198,7 +1198,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("transferToRouteWithManager",
                         "managerId: %d, session: %s, routeId: %s",
                         managerRecord.mManagerId, uniqueSessionId, route.getId()));
@@ -1224,7 +1224,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("setSessionVolumeWithManager",
                         "managerId: %d, session: %s, volume: %d",
                         managerRecord.mManagerId, uniqueSessionId, volume));
@@ -1245,7 +1245,7 @@
             return;
         }
 
-        mEventLogger.log(
+        mEventLogger.enqueue(
                 EventLogger.StringEvent.from("releaseSessionWithManager",
                         "managerId: %d, session: %s",
                         managerRecord.mManagerId, uniqueSessionId));
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
new file mode 100644
index 0000000..df95f86
--- /dev/null
+++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
@@ -0,0 +1,130 @@
+/*
+ * 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.pm;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.IBackgroundInstallControlService;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.SparseArrayMap;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.SystemService;
+
+/**
+ * @hide
+ */
+public class BackgroundInstallControlService extends SystemService {
+    private static final String TAG = "BackgroundInstallControlService";
+
+    private final Context mContext;
+    private final BinderService mBinderService;
+    private final IPackageManager mIPackageManager;
+
+    // User ID -> package name -> time diff
+    // The time diff between the last foreground activity installer and
+    // the "onPackageAdded" function call.
+    private final SparseArrayMap<String, Long> mBackgroundInstalledPackages =
+            new SparseArrayMap<>();
+
+    public BackgroundInstallControlService(@NonNull Context context) {
+        this(new InjectorImpl(context));
+    }
+
+    @VisibleForTesting
+    BackgroundInstallControlService(@NonNull Injector injector) {
+        super(injector.getContext());
+        mContext = injector.getContext();
+        mIPackageManager = injector.getIPackageManager();
+        mBinderService = new BinderService(this);
+    }
+
+    private static final class BinderService extends IBackgroundInstallControlService.Stub {
+        final BackgroundInstallControlService mService;
+
+        BinderService(BackgroundInstallControlService service)  {
+            mService = service;
+        }
+
+        @Override
+        public ParceledListSlice<PackageInfo> getBackgroundInstalledPackages(
+                @PackageManager.PackageInfoFlagsBits long flags, int userId) {
+            ParceledListSlice<PackageInfo> packages;
+            try {
+                packages = mService.mIPackageManager.getInstalledPackages(flags, userId);
+            } catch (RemoteException e) {
+                throw new IllegalStateException("Package manager not available", e);
+            }
+
+            // TODO(b/244216300): to enable the test the usage by BinaryTransparencyService,
+            // we currently comment out the actual implementation.
+            // The fake implementation is just to filter out the first app of the list.
+            // for (int i = 0, size = packages.getList().size(); i < size; i++) {
+            //     String packageName = packages.getList().get(i).packageName;
+            //     if (!mBackgroundInstalledPackages.contains(userId, packageName) {
+            //         packages.getList().remove(i);
+            //     }
+            // }
+            if (packages.getList().size() > 0) {
+                packages.getList().remove(0);
+            }
+            return packages;
+        }
+    }
+
+    /**
+     * Called when the system service should publish a binder service using
+     * {@link #publishBinderService(String, IBinder).}
+     */
+    @Override
+    public void onStart() {
+        publishBinderService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE, mBinderService);
+    }
+
+    /**
+     * Dependency injector for {@link #BackgroundInstallControlService)}.
+     */
+    interface Injector {
+        Context getContext();
+
+        IPackageManager getIPackageManager();
+    }
+
+    private static final class InjectorImpl implements Injector {
+        private final Context mContext;
+
+        InjectorImpl(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public Context getContext() {
+            return mContext;
+        }
+
+        @Override
+        public IPackageManager getIPackageManager() {
+            return IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/pm/OWNERS b/services/core/java/com/android/server/pm/OWNERS
index 8534fab..84324f2 100644
--- a/services/core/java/com/android/server/pm/OWNERS
+++ b/services/core/java/com/android/server/pm/OWNERS
@@ -3,8 +3,6 @@
 jsharkey@android.com
 jsharkey@google.com
 narayan@google.com
-svetoslavganov@android.com
-svetoslavganov@google.com
 include /PACKAGE_MANAGER_OWNERS
 
 # apex support
@@ -26,16 +24,10 @@
 per-file PackageUsage.java = file:dex/OWNERS
 
 # multi user / cross profile
-per-file CrossProfileAppsServiceImpl.java = omakoto@google.com, yamasani@google.com
-per-file CrossProfileAppsService.java = omakoto@google.com, yamasani@google.com
-per-file CrossProfileIntentFilter.java = omakoto@google.com, yamasani@google.com
-per-file CrossProfileIntentResolver.java = omakoto@google.com, yamasani@google.com
+per-file CrossProfile* = file:MULTIUSER_AND_ENTERPRISE_OWNERS
 per-file RestrictionsSet.java = file:MULTIUSER_AND_ENTERPRISE_OWNERS
-per-file UserManager* = file:/MULTIUSER_OWNERS
 per-file UserRestriction* = file:MULTIUSER_AND_ENTERPRISE_OWNERS
-per-file UserSystemPackageInstaller* = file:/MULTIUSER_OWNERS
-per-file UserTypeDetails.java = file:/MULTIUSER_OWNERS
-per-file UserTypeFactory.java = file:/MULTIUSER_OWNERS
+per-file User* = file:/MULTIUSER_OWNERS
 
 # security
 per-file KeySetHandle.java = cbrubaker@google.com, nnk@google.com
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 2bfee8c..cf0ea43 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -104,7 +104,6 @@
 import android.util.TimeUtils;
 import android.util.TypedValue;
 import android.util.Xml;
-import android.view.Display;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -625,14 +624,7 @@
     @GuardedBy("mUserStates")
     private final WatchedUserStates mUserStates = new WatchedUserStates();
 
-    /**
-     * Set on on devices that support background users (key) running on secondary displays (value).
-     */
-    // TODO(b/244644281): move such logic to a different class (like UserDisplayAssigner)
-    @Nullable
-    @GuardedBy("mUsersOnSecondaryDisplays")
-    private final SparseIntArray mUsersOnSecondaryDisplays;
-    private final boolean mUsersOnSecondaryDisplaysEnabled;
+    private final UserVisibilityMediator mUserVisibilityMediator;
 
     private static UserManagerService sInstance;
 
@@ -708,8 +700,7 @@
     @VisibleForTesting
     UserManagerService(Context context) {
         this(context, /* pm= */ null, /* userDataPreparer= */ null,
-                /* packagesLock= */ new Object(), context.getCacheDir(), /* users= */ null,
-                /* usersOnSecondaryDisplays= */ null);
+                /* packagesLock= */ new Object(), context.getCacheDir(), /* users= */ null);
     }
 
     /**
@@ -720,13 +711,13 @@
     UserManagerService(Context context, PackageManagerService pm, UserDataPreparer userDataPreparer,
             Object packagesLock) {
         this(context, pm, userDataPreparer, packagesLock, Environment.getDataDirectory(),
-                /* users= */ null, /* usersOnSecondaryDisplays= */ null);
+                /* users= */ null);
     }
 
     @VisibleForTesting
     UserManagerService(Context context, PackageManagerService pm,
             UserDataPreparer userDataPreparer, Object packagesLock, File dataDir,
-            SparseArray<UserData> users, @Nullable SparseIntArray usersOnSecondaryDisplays) {
+            SparseArray<UserData> users) {
         mContext = context;
         mPm = pm;
         mPackagesLock = packagesLock;
@@ -756,14 +747,7 @@
         mUserStates.put(UserHandle.USER_SYSTEM, UserState.STATE_BOOTING);
         mUser0Allocations = DBG_ALLOCATION ? new AtomicInteger() : null;
         emulateSystemUserModeIfNeeded();
-        mUsersOnSecondaryDisplaysEnabled = UserManager.isUsersOnSecondaryDisplaysEnabled();
-        if (mUsersOnSecondaryDisplaysEnabled) {
-            mUsersOnSecondaryDisplays = usersOnSecondaryDisplays == null
-                    ? new SparseIntArray() // default behavior
-                    : usersOnSecondaryDisplays; // passed by unit test
-        } else {
-            mUsersOnSecondaryDisplays = null;
-        }
+        mUserVisibilityMediator = new UserVisibilityMediator(this);
     }
 
     void systemReady() {
@@ -1652,7 +1636,8 @@
         return isProfileUnchecked(userId);
     }
 
-    private boolean isProfileUnchecked(@UserIdInt int userId) {
+    // TODO(b/244644281): make it private once UserVisibilityMediator don't use it anymore
+    boolean isProfileUnchecked(@UserIdInt int userId) {
         synchronized (mUsersLock) {
             UserInfo userInfo = getUserInfoLU(userId);
             return userInfo != null && userInfo.isProfile();
@@ -1729,11 +1714,6 @@
         return userId == getCurrentUserId();
     }
 
-    @VisibleForTesting
-    boolean isUsersOnSecondaryDisplaysEnabled() {
-        return mUsersOnSecondaryDisplaysEnabled;
-    }
-
     @Override
     public boolean isUserVisible(@UserIdInt int userId) {
         int callingUserId = UserHandle.getCallingUserId();
@@ -1744,69 +1724,7 @@
                     + ") is visible");
         }
 
-        return isUserVisibleUnchecked(userId);
-    }
-
-    @VisibleForTesting
-    boolean isUserVisibleUnchecked(@UserIdInt int userId) {
-        // First check current foreground user and their profiles (on main display)
-        if (isCurrentUserOrRunningProfileOfCurrentUser(userId)) {
-            return true;
-        }
-
-        // Device doesn't support multiple users on multiple displays, so only users checked above
-        // can be visible
-        if (!mUsersOnSecondaryDisplaysEnabled) {
-            return false;
-        }
-
-        synchronized (mUsersOnSecondaryDisplays) {
-            return mUsersOnSecondaryDisplays.indexOfKey(userId) >= 0;
-        }
-    }
-
-    @VisibleForTesting
-    int getDisplayAssignedToUser(@UserIdInt int userId) {
-        if (isCurrentUserOrRunningProfileOfCurrentUser(userId)) {
-            return Display.DEFAULT_DISPLAY;
-        }
-
-        if (!mUsersOnSecondaryDisplaysEnabled) {
-            return Display.INVALID_DISPLAY;
-        }
-
-        synchronized (mUsersOnSecondaryDisplays) {
-            return mUsersOnSecondaryDisplays.get(userId, Display.INVALID_DISPLAY);
-        }
-    }
-
-    @VisibleForTesting
-    int getUserAssignedToDisplay(int displayId) {
-        if (displayId == Display.DEFAULT_DISPLAY || !mUsersOnSecondaryDisplaysEnabled) {
-            return getCurrentUserId();
-        }
-
-        synchronized (mUsersOnSecondaryDisplays) {
-            for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) {
-                if (mUsersOnSecondaryDisplays.valueAt(i) != displayId) {
-                    continue;
-                }
-                int userId = mUsersOnSecondaryDisplays.keyAt(i);
-                if (!isProfileUnchecked(userId)) {
-                    return userId;
-                } else if (DBG_MUMD) {
-                    Slogf.d(LOG_TAG, "getUserAssignedToDisplay(%d): skipping user %d because it's "
-                            + "a profile", displayId, userId);
-                }
-            }
-        }
-
-        int currentUserId = getCurrentUserId();
-        if (DBG_MUMD) {
-            Slogf.d(LOG_TAG, "getUserAssignedToDisplay(%d): no user assigned to display, returning "
-                    + "current user (%d) instead", displayId, currentUserId);
-        }
-        return currentUserId;
+        return mUserVisibilityMediator.isUserVisible(userId);
     }
 
     /**
@@ -1850,54 +1768,9 @@
         return false;
     }
 
-    // TODO(b/239982558): try to merge with isUserVisibleUnchecked() (once both are unit tested)
-    /**
-     * See {@link UserManagerInternal#isUserVisible(int, int)}.
-     */
+    // Called by UserManagerServiceShellCommand
     boolean isUserVisibleOnDisplay(@UserIdInt int userId, int displayId) {
-        if (displayId == Display.INVALID_DISPLAY) {
-            return false;
-        }
-        if (!mUsersOnSecondaryDisplaysEnabled) {
-            return isCurrentUserOrRunningProfileOfCurrentUser(userId);
-        }
-
-        // TODO(b/244644281): temporary workaround to let WM use this API without breaking current
-        // behavior - return true for current user / profile for any display (other than those
-        // explicitly assigned to another users), otherwise they wouldn't be able to launch
-        // activities on other non-passenger displays, like cluster, display, or virtual displays).
-        // In the long-term, it should rely just on mUsersOnSecondaryDisplays, which
-        // would be updated by DisplayManagerService when displays are created / initialized.
-        if (isCurrentUserOrRunningProfileOfCurrentUser(userId)) {
-            synchronized (mUsersOnSecondaryDisplays) {
-                boolean assignedToUser = false;
-                boolean assignedToAnotherUser = false;
-                for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) {
-                    if (mUsersOnSecondaryDisplays.valueAt(i) == displayId) {
-                        if (mUsersOnSecondaryDisplays.keyAt(i) == userId) {
-                            assignedToUser = true;
-                            break;
-                        } else {
-                            assignedToAnotherUser = true;
-                            // Cannot break because it could be assigned to a profile of the user
-                            // (and we better not assume that the iteration will check for the
-                            // parent user before its profiles)
-                        }
-                    }
-                }
-                if (DBG_MUMD) {
-                    Slogf.d(LOG_TAG, "isUserVisibleOnDisplay(%d, %d): assignedToUser=%b, "
-                            + "assignedToAnotherUser=%b, mUsersOnSecondaryDisplays=%s",
-                            userId, displayId, assignedToUser, assignedToAnotherUser,
-                            mUsersOnSecondaryDisplays);
-                }
-                return assignedToUser || !assignedToAnotherUser;
-            }
-        }
-
-        synchronized (mUsersOnSecondaryDisplays) {
-            return mUsersOnSecondaryDisplays.get(userId, Display.INVALID_DISPLAY) == displayId;
-        }
+        return mUserVisibilityMediator.isUserVisible(userId, displayId);
     }
 
     @Override
@@ -1915,7 +1788,7 @@
                 for (int i = 0; i < usersSize; i++) {
                     UserInfo ui = mUsers.valueAt(i).info;
                     if (!ui.partial && !ui.preCreated && !mRemovingUserIds.get(ui.id)
-                            && isUserVisibleUnchecked(ui.id)) {
+                            && mUserVisibilityMediator.isUserVisible(ui.id)) {
                         visibleUsers.add(UserHandle.of(ui.id));
                     }
                 }
@@ -6251,9 +6124,15 @@
         final long nowRealtime = SystemClock.elapsedRealtime();
         final StringBuilder sb = new StringBuilder();
 
-        if (args != null && args.length > 0 && args[0].equals("--user")) {
-            dumpUser(pw, UserHandle.parseUserArg(args[1]), sb, now, nowRealtime);
-            return;
+        if (args != null && args.length > 0) {
+            switch (args[0]) {
+                case "--user":
+                    dumpUser(pw, UserHandle.parseUserArg(args[1]), sb, now, nowRealtime);
+                    return;
+                case "--visibility-mediator":
+                    mUserVisibilityMediator.dump(pw);
+                    return;
+            }
         }
 
         final int currentUserId = getCurrentUserId();
@@ -6316,18 +6195,9 @@
             }
         } // synchronized (mPackagesLock)
 
-        // Multiple Users on Multiple Display info
-        pw.println("  Supports users on secondary displays: " + mUsersOnSecondaryDisplaysEnabled);
-        // mUsersOnSecondaryDisplaysEnabled is set on constructor, while the UserManager API is
-        // set dynamically, so print both to help cases where the developer changed it on the fly
-        pw.println("  UM.isUsersOnSecondaryDisplaysEnabled(): "
-                + UserManager.isUsersOnSecondaryDisplaysEnabled());
-        if (mUsersOnSecondaryDisplaysEnabled) {
-            pw.print("  Users on secondary displays: ");
-            synchronized (mUsersOnSecondaryDisplays) {
-                pw.println(mUsersOnSecondaryDisplays);
-            }
-        }
+        pw.println();
+        mUserVisibilityMediator.dump(pw);
+        pw.println();
 
         // Dump some capabilities
         pw.println();
@@ -6899,133 +6769,33 @@
         }
 
         @Override
-        public void assignUserToDisplay(int userId, int displayId) {
-            if (DBG_MUMD) {
-                Slogf.d(LOG_TAG, "assignUserToDisplay(%d, %d)", userId, displayId);
-            }
-
-            // NOTE: Using Boolean instead of boolean as it will be re-used below
-            Boolean isProfile = null;
-            if (displayId == Display.DEFAULT_DISPLAY) {
-                if (mUsersOnSecondaryDisplaysEnabled) {
-                    // Profiles are only supported in the default display, but it cannot return yet
-                    // as it needs to check if the parent is also assigned to the DEFAULT_DISPLAY
-                    // (this is done indirectly below when it checks that the profile parent is the
-                    // current user, as the current user is always assigned to the DEFAULT_DISPLAY).
-                    isProfile = isProfileUnchecked(userId);
-                }
-                if (isProfile == null || !isProfile) {
-                    // Don't need to do anything because methods (such as isUserVisible()) already
-                    // know that the current user (and their profiles) is assigned to the default
-                    // display.
-                    if (DBG_MUMD) {
-                        Slogf.d(LOG_TAG, "ignoring on default display");
-                    }
-                    return;
-                }
-            }
-
-            if (!mUsersOnSecondaryDisplaysEnabled) {
-                throw new UnsupportedOperationException("assignUserToDisplay(" + userId + ", "
-                        + displayId + ") called on device that doesn't support multiple "
-                        + "users on multiple displays");
-            }
-
-            Preconditions.checkArgument(userId != UserHandle.USER_SYSTEM, "Cannot assign system "
-                    + "user to secondary display (%d)", displayId);
-            Preconditions.checkArgument(displayId != Display.INVALID_DISPLAY,
-                    "Cannot assign to INVALID_DISPLAY (%d)", displayId);
-
-            int currentUserId = getCurrentUserId();
-            Preconditions.checkArgument(userId != currentUserId,
-                    "Cannot assign current user (%d) to other displays", currentUserId);
-
-            if (isProfile == null) {
-                isProfile = isProfileUnchecked(userId);
-            }
-            synchronized (mUsersOnSecondaryDisplays) {
-                if (isProfile) {
-                    // Profile can only start in the same display as parent. And for simplicity,
-                    // that display must be the DEFAULT_DISPLAY.
-                    Preconditions.checkArgument(displayId == Display.DEFAULT_DISPLAY,
-                            "Profile user can only be started in the default display");
-                    int parentUserId = getProfileParentId(userId);
-                    Preconditions.checkArgument(parentUserId == currentUserId,
-                            "Only profile of current user can be assigned to a display");
-                    if (DBG_MUMD) {
-                        Slogf.d(LOG_TAG, "Ignoring profile user %d on default display", userId);
-                    }
-                    return;
-                }
-
-                // Check if display is available
-                for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) {
-                    int assignedUserId = mUsersOnSecondaryDisplays.keyAt(i);
-                    int assignedDisplayId = mUsersOnSecondaryDisplays.valueAt(i);
-                    if (DBG_MUMD) {
-                        Slogf.d(LOG_TAG, "%d: assignedUserId=%d, assignedDisplayId=%d",
-                                i, assignedUserId, assignedDisplayId);
-                    }
-                    if (displayId == assignedDisplayId) {
-                        throw new IllegalStateException("Cannot assign user " + userId + " to "
-                                + "display " + displayId + " because such display is already "
-                                + "assigned to user " + assignedUserId);
-                    }
-                    if (userId == assignedUserId) {
-                        throw new IllegalStateException("Cannot assign user " + userId + " to "
-                                + "display " + displayId + " because such user is as already "
-                                + "assigned to display " + assignedDisplayId);
-                    }
-                }
-
-                if (DBG_MUMD) {
-                    Slogf.d(LOG_TAG, "Adding full user %d -> display %d", userId, displayId);
-                }
-                mUsersOnSecondaryDisplays.put(userId, displayId);
-            }
+        public void assignUserToDisplay(@UserIdInt int userId, int displayId) {
+            mUserVisibilityMediator.assignUserToDisplay(userId, displayId);
         }
 
         @Override
         public void unassignUserFromDisplay(@UserIdInt int userId) {
-            if (DBG_MUMD) {
-                Slogf.d(LOG_TAG, "unassignUserFromDisplay(%d)", userId);
-            }
-            if (!mUsersOnSecondaryDisplaysEnabled) {
-                // Don't need to do anything because methods (such as isUserVisible()) already know
-                // that the current user (and their profiles) is assigned to the default display.
-                if (DBG_MUMD) {
-                    Slogf.d(LOG_TAG, "ignoring when device doesn't support MUMD");
-                }
-                return;
-            }
-
-            synchronized (mUsersOnSecondaryDisplays) {
-                if (DBG_MUMD) {
-                    Slogf.d(LOG_TAG, "Removing %d from mUsersOnSecondaryDisplays (%s)", userId,
-                            mUsersOnSecondaryDisplays);
-                }
-                mUsersOnSecondaryDisplays.delete(userId);
-            }
+            mUserVisibilityMediator.unassignUserFromDisplay(userId);
         }
 
         @Override
-        public boolean isUserVisible(int userId) {
-            return isUserVisibleUnchecked(userId);
+        public boolean isUserVisible(@UserIdInt int userId) {
+            return mUserVisibilityMediator.isUserVisible(userId);
         }
 
         @Override
-        public boolean isUserVisible(int userId, int displayId) {
-            return isUserVisibleOnDisplay(userId, displayId);
+        public boolean isUserVisible(@UserIdInt int userId, int displayId) {
+            return mUserVisibilityMediator.isUserVisible(userId, displayId);
         }
 
         @Override
-        public int getDisplayAssignedToUser(int userId) {
-            return UserManagerService.this.getDisplayAssignedToUser(userId);
+        public int getDisplayAssignedToUser(@UserIdInt int userId) {
+            return mUserVisibilityMediator.getDisplayAssignedToUser(userId);
         }
 
         @Override
-        public int getUserAssignedToDisplay(int displayId) {
-            return UserManagerService.this.getUserAssignedToDisplay(displayId);
+        public @UserIdInt int getUserAssignedToDisplay(int displayId) {
+            return mUserVisibilityMediator.getUserAssignedToDisplay(displayId);
         }
     } // class LocalService
 
diff --git a/services/core/java/com/android/server/pm/UserVisibilityMediator.java b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
new file mode 100644
index 0000000..f725c48
--- /dev/null
+++ b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
@@ -0,0 +1,350 @@
+/*
+ * 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.pm;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.IndentingPrintWriter;
+import android.util.SparseIntArray;
+import android.view.Display;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+import com.android.server.utils.Slogf;
+
+import java.io.PrintWriter;
+
+/**
+ * Class responsible for deciding whether a user is visible (or visible for a given display).
+ *
+ * <p>This class is thread safe.
+ */
+// TODO(b/244644281): improve javadoc (for example, explain all cases / modes)
+public final class UserVisibilityMediator {
+
+    private static final boolean DBG = false; // DO NOT SUBMIT WITH TRUE
+
+    private static final String TAG = UserVisibilityMediator.class.getSimpleName();
+
+    private final Object mLock = new Object();
+
+    // TODO(b/244644281): should not depend on service, but keep its own internal state (like
+    // current user and profile groups), but it is initially as the code was just moved from UMS
+    // "as is". Similarly, it shouldn't need to pass the SparseIntArray on constructor (which was
+    // added to UMS for testing purposes)
+    private final UserManagerService mService;
+
+    private final boolean mUsersOnSecondaryDisplaysEnabled;
+
+    @Nullable
+    @GuardedBy("mLock")
+    private final SparseIntArray mUsersOnSecondaryDisplays;
+
+    UserVisibilityMediator(UserManagerService service) {
+        this(service, UserManager.isUsersOnSecondaryDisplaysEnabled(),
+                /* usersOnSecondaryDisplays= */ null);
+    }
+
+    @VisibleForTesting
+    UserVisibilityMediator(UserManagerService service, boolean usersOnSecondaryDisplaysEnabled,
+            @Nullable SparseIntArray usersOnSecondaryDisplays) {
+        mService = service;
+        mUsersOnSecondaryDisplaysEnabled = usersOnSecondaryDisplaysEnabled;
+        if (mUsersOnSecondaryDisplaysEnabled) {
+            mUsersOnSecondaryDisplays = usersOnSecondaryDisplays == null
+                    ? new SparseIntArray() // default behavior
+                    : usersOnSecondaryDisplays; // passed by unit test
+        } else {
+            mUsersOnSecondaryDisplays = null;
+        }
+    }
+
+    /**
+     * See {@link UserManagerInternal#assignUserToDisplay(int, int)}.
+     */
+    public void assignUserToDisplay(int userId, int displayId) {
+        if (DBG) {
+            Slogf.d(TAG, "assignUserToDisplay(%d, %d)", userId, displayId);
+        }
+
+        // NOTE: Using Boolean instead of boolean as it will be re-used below
+        Boolean isProfile = null;
+        if (displayId == Display.DEFAULT_DISPLAY) {
+            if (mUsersOnSecondaryDisplaysEnabled) {
+                // Profiles are only supported in the default display, but it cannot return yet
+                // as it needs to check if the parent is also assigned to the DEFAULT_DISPLAY
+                // (this is done indirectly below when it checks that the profile parent is the
+                // current user, as the current user is always assigned to the DEFAULT_DISPLAY).
+                isProfile = isProfileUnchecked(userId);
+            }
+            if (isProfile == null || !isProfile) {
+                // Don't need to do anything because methods (such as isUserVisible()) already
+                // know that the current user (and their profiles) is assigned to the default
+                // display.
+                if (DBG) {
+                    Slogf.d(TAG, "ignoring on default display");
+                }
+                return;
+            }
+        }
+
+        if (!mUsersOnSecondaryDisplaysEnabled) {
+            throw new UnsupportedOperationException("assignUserToDisplay(" + userId + ", "
+                    + displayId + ") called on device that doesn't support multiple "
+                    + "users on multiple displays");
+        }
+
+        Preconditions.checkArgument(userId != UserHandle.USER_SYSTEM, "Cannot assign system "
+                + "user to secondary display (%d)", displayId);
+        Preconditions.checkArgument(displayId != Display.INVALID_DISPLAY,
+                "Cannot assign to INVALID_DISPLAY (%d)", displayId);
+
+        int currentUserId = getCurrentUserId();
+        Preconditions.checkArgument(userId != currentUserId,
+                "Cannot assign current user (%d) to other displays", currentUserId);
+
+        if (isProfile == null) {
+            isProfile = isProfileUnchecked(userId);
+        }
+        synchronized (mLock) {
+            if (isProfile) {
+                // Profile can only start in the same display as parent. And for simplicity,
+                // that display must be the DEFAULT_DISPLAY.
+                Preconditions.checkArgument(displayId == Display.DEFAULT_DISPLAY,
+                        "Profile user can only be started in the default display");
+                int parentUserId = getProfileParentId(userId);
+                Preconditions.checkArgument(parentUserId == currentUserId,
+                        "Only profile of current user can be assigned to a display");
+                if (DBG) {
+                    Slogf.d(TAG, "Ignoring profile user %d on default display", userId);
+                }
+                return;
+            }
+
+            // Check if display is available
+            for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) {
+                int assignedUserId = mUsersOnSecondaryDisplays.keyAt(i);
+                int assignedDisplayId = mUsersOnSecondaryDisplays.valueAt(i);
+                if (DBG) {
+                    Slogf.d(TAG, "%d: assignedUserId=%d, assignedDisplayId=%d",
+                            i, assignedUserId, assignedDisplayId);
+                }
+                if (displayId == assignedDisplayId) {
+                    throw new IllegalStateException("Cannot assign user " + userId + " to "
+                            + "display " + displayId + " because such display is already "
+                            + "assigned to user " + assignedUserId);
+                }
+                if (userId == assignedUserId) {
+                    throw new IllegalStateException("Cannot assign user " + userId + " to "
+                            + "display " + displayId + " because such user is as already "
+                            + "assigned to display " + assignedDisplayId);
+                }
+            }
+
+            if (DBG) {
+                Slogf.d(TAG, "Adding full user %d -> display %d", userId, displayId);
+            }
+            mUsersOnSecondaryDisplays.put(userId, displayId);
+        }
+    }
+
+    /**
+     * See {@link UserManagerInternal#unassignUserFromDisplay(int)}.
+     */
+    public void unassignUserFromDisplay(int userId) {
+        if (DBG) {
+            Slogf.d(TAG, "unassignUserFromDisplay(%d)", userId);
+        }
+        if (!mUsersOnSecondaryDisplaysEnabled) {
+            // Don't need to do anything because methods (such as isUserVisible()) already know
+            // that the current user (and their profiles) is assigned to the default display.
+            if (DBG) {
+                Slogf.d(TAG, "ignoring when device doesn't support MUMD");
+            }
+            return;
+        }
+
+        synchronized (mLock) {
+            if (DBG) {
+                Slogf.d(TAG, "Removing %d from mUsersOnSecondaryDisplays (%s)", userId,
+                        mUsersOnSecondaryDisplays);
+            }
+            mUsersOnSecondaryDisplays.delete(userId);
+        }
+    }
+
+    /**
+     * See {@link UserManagerInternal#isUserVisible(int)}.
+     */
+    public boolean isUserVisible(int userId) {
+        // First check current foreground user and their profiles (on main display)
+        if (isCurrentUserOrRunningProfileOfCurrentUser(userId)) {
+            return true;
+        }
+
+        // Device doesn't support multiple users on multiple displays, so only users checked above
+        // can be visible
+        if (!mUsersOnSecondaryDisplaysEnabled) {
+            return false;
+        }
+
+        synchronized (mLock) {
+            return mUsersOnSecondaryDisplays.indexOfKey(userId) >= 0;
+        }
+    }
+
+    /**
+     * See {@link UserManagerInternal#isUserVisible(int, int)}.
+     */
+    public boolean isUserVisible(int userId, int displayId) {
+        if (displayId == Display.INVALID_DISPLAY) {
+            return false;
+        }
+        if (!mUsersOnSecondaryDisplaysEnabled) {
+            return isCurrentUserOrRunningProfileOfCurrentUser(userId);
+        }
+
+        // TODO(b/244644281): temporary workaround to let WM use this API without breaking current
+        // behavior - return true for current user / profile for any display (other than those
+        // explicitly assigned to another users), otherwise they wouldn't be able to launch
+        // activities on other non-passenger displays, like cluster, display, or virtual displays).
+        // In the long-term, it should rely just on mUsersOnSecondaryDisplays, which
+        // would be updated by DisplayManagerService when displays are created / initialized.
+        if (isCurrentUserOrRunningProfileOfCurrentUser(userId)) {
+            synchronized (mLock) {
+                boolean assignedToUser = false;
+                boolean assignedToAnotherUser = false;
+                for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) {
+                    if (mUsersOnSecondaryDisplays.valueAt(i) == displayId) {
+                        if (mUsersOnSecondaryDisplays.keyAt(i) == userId) {
+                            assignedToUser = true;
+                            break;
+                        } else {
+                            assignedToAnotherUser = true;
+                            // Cannot break because it could be assigned to a profile of the user
+                            // (and we better not assume that the iteration will check for the
+                            // parent user before its profiles)
+                        }
+                    }
+                }
+                if (DBG) {
+                    Slogf.d(TAG, "isUserVisibleOnDisplay(%d, %d): assignedToUser=%b, "
+                            + "assignedToAnotherUser=%b, mUsersOnSecondaryDisplays=%s",
+                            userId, displayId, assignedToUser, assignedToAnotherUser,
+                            mUsersOnSecondaryDisplays);
+                }
+                return assignedToUser || !assignedToAnotherUser;
+            }
+        }
+
+        synchronized (mLock) {
+            return mUsersOnSecondaryDisplays.get(userId, Display.INVALID_DISPLAY) == displayId;
+        }
+    }
+
+    /**
+     * See {@link UserManagerInternal#getDisplayAssignedToUser(int)}.
+     */
+    public int getDisplayAssignedToUser(int userId) {
+        if (isCurrentUserOrRunningProfileOfCurrentUser(userId)) {
+            return Display.DEFAULT_DISPLAY;
+        }
+
+        if (!mUsersOnSecondaryDisplaysEnabled) {
+            return Display.INVALID_DISPLAY;
+        }
+
+        synchronized (mLock) {
+            return mUsersOnSecondaryDisplays.get(userId, Display.INVALID_DISPLAY);
+        }
+    }
+
+    /**
+     * See {@link UserManagerInternal#getUserAssignedToDisplay(int)}.
+     */
+    public int getUserAssignedToDisplay(int displayId) {
+        if (displayId == Display.DEFAULT_DISPLAY || !mUsersOnSecondaryDisplaysEnabled) {
+            return getCurrentUserId();
+        }
+
+        synchronized (mLock) {
+            for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) {
+                if (mUsersOnSecondaryDisplays.valueAt(i) != displayId) {
+                    continue;
+                }
+                int userId = mUsersOnSecondaryDisplays.keyAt(i);
+                if (!isProfileUnchecked(userId)) {
+                    return userId;
+                } else if (DBG) {
+                    Slogf.d(TAG, "getUserAssignedToDisplay(%d): skipping user %d because it's "
+                            + "a profile", displayId, userId);
+                }
+            }
+        }
+
+        int currentUserId = getCurrentUserId();
+        if (DBG) {
+            Slogf.d(TAG, "getUserAssignedToDisplay(%d): no user assigned to display, returning "
+                    + "current user (%d) instead", displayId, currentUserId);
+        }
+        return currentUserId;
+    }
+
+    private void dump(IndentingPrintWriter ipw) {
+        ipw.println("UserVisibilityManager");
+        ipw.increaseIndent();
+
+        ipw.print("Supports users on secondary displays: ");
+        ipw.println(mUsersOnSecondaryDisplaysEnabled);
+
+        if (mUsersOnSecondaryDisplaysEnabled) {
+            ipw.print("Users on secondary displays: ");
+            synchronized (mLock) {
+                ipw.println(mUsersOnSecondaryDisplays);
+            }
+        }
+
+        ipw.decreaseIndent();
+    }
+
+    void dump(PrintWriter pw) {
+        if (pw instanceof IndentingPrintWriter) {
+            dump((IndentingPrintWriter) pw);
+            return;
+        }
+        dump(new IndentingPrintWriter(pw));
+    }
+
+    // TODO(b/244644281): remove methods below once this class caches that state
+    private @UserIdInt int getCurrentUserId() {
+        return mService.getCurrentUserId();
+    }
+
+    private boolean isCurrentUserOrRunningProfileOfCurrentUser(@UserIdInt int userId) {
+        return mService.isCurrentUserOrRunningProfileOfCurrentUser(userId);
+    }
+
+    private boolean isProfileUnchecked(@UserIdInt int userId) {
+        return mService.isProfileUnchecked(userId);
+    }
+
+    private @UserIdInt int getProfileParentId(@UserIdInt int userId) {
+        return mService.getProfileParentId(userId);
+    }
+}
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index 799ef41..ab223ef 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -4245,7 +4245,6 @@
         }
         boolean changed = false;
 
-        Set<Permission> needsUpdate = null;
         synchronized (mLock) {
             final Iterator<Permission> it = mRegistry.getPermissionTrees().iterator();
             while (it.hasNext()) {
@@ -4264,26 +4263,6 @@
                             + " that used to be declared by " + bp.getPackageName());
                     it.remove();
                 }
-                if (needsUpdate == null) {
-                    needsUpdate = new ArraySet<>();
-                }
-                needsUpdate.add(bp);
-            }
-        }
-        if (needsUpdate != null) {
-            for (final Permission bp : needsUpdate) {
-                final AndroidPackage sourcePkg =
-                        mPackageManagerInt.getPackage(bp.getPackageName());
-                final PackageStateInternal sourcePs =
-                        mPackageManagerInt.getPackageStateInternal(bp.getPackageName());
-                synchronized (mLock) {
-                    if (sourcePkg != null && sourcePs != null) {
-                        continue;
-                    }
-                    Slog.w(TAG, "Removing dangling permission tree: " + bp.getName()
-                            + " from package " + bp.getPackageName());
-                    mRegistry.removePermission(bp.getName());
-                }
             }
         }
         return changed;
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index c4e122d..98b5c1b 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -256,6 +256,7 @@
     static final int SHORT_PRESS_POWER_GO_HOME = 4;
     static final int SHORT_PRESS_POWER_CLOSE_IME_OR_GO_HOME = 5;
     static final int SHORT_PRESS_POWER_LOCK_OR_SLEEP = 6;
+    static final int SHORT_PRESS_POWER_DREAM_OR_SLEEP = 7;
 
     // must match: config_LongPressOnPowerBehavior in config.xml
     static final int LONG_PRESS_POWER_NOTHING = 0;
@@ -969,7 +970,14 @@
             powerMultiPressAction(eventTime, interactive, mTriplePressOnPowerBehavior);
         } else if (count > 3 && count <= getMaxMultiPressPowerCount()) {
             Slog.d(TAG, "No behavior defined for power press count " + count);
-        } else if (count == 1 && interactive && !beganFromNonInteractive) {
+        } else if (count == 1 && interactive) {
+            if (beganFromNonInteractive) {
+                // The "screen is off" case, where we might want to start dreaming on power button
+                // press.
+                attemptToDreamFromShortPowerButtonPress(false, () -> {});
+                return;
+            }
+
             if (mSideFpsEventHandler.shouldConsumeSinglePress(eventTime)) {
                 Slog.i(TAG, "Suppressing power key because the user is interacting with the "
                         + "fingerprint sensor");
@@ -1018,11 +1026,39 @@
                     }
                     break;
                 }
+                case SHORT_PRESS_POWER_DREAM_OR_SLEEP: {
+                    attemptToDreamFromShortPowerButtonPress(
+                            true,
+                            () -> sleepDefaultDisplayFromPowerButton(eventTime, 0));
+                    break;
+                }
             }
         }
     }
 
     /**
+     * Attempt to dream from a power button press.
+     *
+     * @param isScreenOn Whether the screen is currently on.
+     * @param noDreamAction The action to perform if dreaming is not possible.
+     */
+    private void attemptToDreamFromShortPowerButtonPress(
+            boolean isScreenOn, Runnable noDreamAction) {
+        if (mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_SLEEP) {
+            noDreamAction.run();
+            return;
+        }
+
+        final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal();
+        if (dreamManagerInternal == null || !dreamManagerInternal.canStartDreaming(isScreenOn)) {
+            noDreamAction.run();
+            return;
+        }
+
+        dreamManagerInternal.requestDream();
+    }
+
+    /**
      * Sends the default display to sleep as a result of a power button press.
      *
      * @return {@code true} if the device was sent to sleep, {@code false} if the device did not
@@ -1593,7 +1629,8 @@
 
         // If there's a dream running then use home to escape the dream
         // but don't actually go home.
-        if (mDreamManagerInternal != null && mDreamManagerInternal.isDreaming()) {
+        final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal();
+        if (dreamManagerInternal != null && dreamManagerInternal.isDreaming()) {
             mDreamManagerInternal.stopDream(false /*immediate*/, "short press on home" /*reason*/);
             return;
         }
@@ -2529,6 +2566,15 @@
         }
     }
 
+    private DreamManagerInternal getDreamManagerInternal() {
+        if (mDreamManagerInternal == null) {
+            // If mDreamManagerInternal is null, attempt to re-fetch it.
+            mDreamManagerInternal = LocalServices.getService(DreamManagerInternal.class);
+        }
+
+        return mDreamManagerInternal;
+    }
+
     private void updateWakeGestureListenerLp() {
         if (shouldEnableWakeGestureLp()) {
             mWakeGestureListener.requestWakeUpTrigger();
diff --git a/services/core/java/com/android/server/utils/EventLogger.java b/services/core/java/com/android/server/utils/EventLogger.java
index 004312f..11766a3 100644
--- a/services/core/java/com/android/server/utils/EventLogger.java
+++ b/services/core/java/com/android/server/utils/EventLogger.java
@@ -43,7 +43,7 @@
     /**
      * The maximum number of events to keep in {@code mEvents}.
      *
-     * <p>Calling {@link #log} when the size of {@link #mEvents} matches the threshold will
+     * <p>Calling {@link #enqueue} when the size of {@link #mEvents} matches the threshold will
      * cause the oldest event to be evicted.
      */
     private final int mMemSize;
@@ -60,7 +60,7 @@
     }
 
     /** Enqueues {@code event} to be logged. */
-    public synchronized void log(Event event) {
+    public synchronized void enqueue(Event event) {
         if (mEvents.size() >= mMemSize) {
             mEvents.removeLast();
         }
@@ -69,24 +69,14 @@
     }
 
     /**
-     * Add a string-based event to the log, and print it to logcat as info.
-     * @param msg the message for the logs
-     * @param tag the logcat tag to use
-     */
-    public synchronized void loglogi(String msg, String tag) {
-        final Event event = new StringEvent(msg);
-        log(event.printLog(tag));
-    }
-
-    /**
-     * Same as {@link #loglogi(String, String)} but specifying the logcat type
+     * Add a string-based event to the log, and print it to logcat with a specific severity.
      * @param msg the message for the logs
      * @param logType the type of logcat entry
      * @param tag the logcat tag to use
      */
-    public synchronized void loglog(String msg, @Event.LogType int logType, String tag) {
+    public synchronized void enqueueAndLog(String msg, @Event.LogType int logType, String tag) {
         final Event event = new StringEvent(msg);
-        log(event.printLog(logType, tag));
+        enqueue(event.printLog(logType, tag));
     }
 
     /** Dumps events using {@link PrintWriter}. */
diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
index 7d84bdf..d7c5e93 100644
--- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
+++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
@@ -424,7 +424,7 @@
         try {
             harmfulAppWarning = mService.getPackageManager()
                     .getHarmfulAppWarning(mAInfo.packageName, mUserId);
-        } catch (RemoteException ex) {
+        } catch (RemoteException | IllegalArgumentException ex) {
             return false;
         }
 
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 3f380e7..0d87237 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -107,6 +107,7 @@
     jmethodID notifyFocusChanged;
     jmethodID notifySensorEvent;
     jmethodID notifySensorAccuracy;
+    jmethodID notifyStylusGestureStarted;
     jmethodID notifyVibratorState;
     jmethodID filterInputEvent;
     jmethodID interceptKeyBeforeQueueing;
@@ -299,6 +300,7 @@
     void requestPointerCapture(const sp<IBinder>& windowToken, bool enabled);
     void setCustomPointerIcon(const SpriteIcon& icon);
     void setMotionClassifierEnabled(bool enabled);
+    std::optional<std::string> getBluetoothAddress(int32_t deviceId);
 
     /* --- InputReaderPolicyInterface implementation --- */
 
@@ -312,6 +314,7 @@
                                                            int32_t surfaceRotation) override;
 
     TouchAffineTransformation getTouchAffineTransformation(JNIEnv* env, jfloatArray matrixArr);
+    void notifyStylusGestureStarted(int32_t deviceId, nsecs_t eventTime) override;
 
     /* --- InputDispatcherPolicyInterface implementation --- */
 
@@ -370,37 +373,37 @@
     Mutex mLock;
     struct Locked {
         // Display size information.
-        std::vector<DisplayViewport> viewports;
+        std::vector<DisplayViewport> viewports{};
 
         // True if System UI is less noticeable.
-        bool systemUiLightsOut;
+        bool systemUiLightsOut{false};
 
         // Pointer speed.
-        int32_t pointerSpeed;
+        int32_t pointerSpeed{0};
 
         // Pointer acceleration.
-        float pointerAcceleration;
+        float pointerAcceleration{android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION};
 
         // True if pointer gestures are enabled.
-        bool pointerGesturesEnabled;
+        bool pointerGesturesEnabled{true};
 
         // Show touches feature enable/disable.
-        bool showTouches;
+        bool showTouches{false};
 
         // The latest request to enable or disable Pointer Capture.
-        PointerCaptureRequest pointerCaptureRequest;
+        PointerCaptureRequest pointerCaptureRequest{};
 
         // Sprite controller singleton, created on first use.
-        sp<SpriteController> spriteController;
+        sp<SpriteController> spriteController{};
 
         // Pointer controller singleton, created and destroyed as needed.
-        std::weak_ptr<PointerController> pointerController;
+        std::weak_ptr<PointerController> pointerController{};
 
         // Input devices to be disabled
-        std::set<int32_t> disabledInputDevices;
+        std::set<int32_t> disabledInputDevices{};
 
         // Associated Pointer controller display.
-        int32_t pointerDisplayId;
+        int32_t pointerDisplayId{ADISPLAY_ID_DEFAULT};
     } mLocked GUARDED_BY(mLock);
 
     std::atomic<bool> mInteractive;
@@ -419,16 +422,6 @@
 
     mServiceObj = env->NewGlobalRef(serviceObj);
 
-    {
-        AutoMutex _l(mLock);
-        mLocked.systemUiLightsOut = false;
-        mLocked.pointerSpeed = 0;
-        mLocked.pointerAcceleration = android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION;
-        mLocked.pointerGesturesEnabled = true;
-        mLocked.showTouches = false;
-        mLocked.pointerDisplayId = ADISPLAY_ID_DEFAULT;
-    }
-    mInteractive = true;
     InputManager* im = new InputManager(this, this);
     mInputManager = im;
     defaultServiceManager()->addService(String16("inputflinger"), im);
@@ -1177,6 +1170,13 @@
     return transform;
 }
 
+void NativeInputManager::notifyStylusGestureStarted(int32_t deviceId, nsecs_t eventTime) {
+    JNIEnv* env = jniEnv();
+    env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyStylusGestureStarted, deviceId,
+                        eventTime);
+    checkAndClearExceptionFromCallback(env, "notifyStylusGestureStarted");
+}
+
 bool NativeInputManager::filterInputEvent(const InputEvent* inputEvent, uint32_t policyFlags) {
     ATRACE_CALL();
     jobject inputEventObj;
@@ -1487,6 +1487,10 @@
     mInputManager->getProcessor().setMotionClassifierEnabled(enabled);
 }
 
+std::optional<std::string> NativeInputManager::getBluetoothAddress(int32_t deviceId) {
+    return mInputManager->getReader().getBluetoothAddress(deviceId);
+}
+
 bool NativeInputManager::isPerDisplayTouchModeEnabled() {
     JNIEnv* env = jniEnv();
     jboolean enabled =
@@ -2326,6 +2330,12 @@
     im->setPointerDisplayId(displayId);
 }
 
+static jstring nativeGetBluetoothAddress(JNIEnv* env, jobject nativeImplObj, jint deviceId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+    const auto address = im->getBluetoothAddress(deviceId);
+    return address ? env->NewStringUTF(address->c_str()) : nullptr;
+}
+
 // ----------------------------------------------------------------------------
 
 static const JNINativeMethod gInputManagerMethods[] = {
@@ -2408,6 +2418,7 @@
         {"flushSensor", "(II)Z", (void*)nativeFlushSensor},
         {"cancelCurrentTouch", "()V", (void*)nativeCancelCurrentTouch},
         {"setPointerDisplayId", "(I)V", (void*)nativeSetPointerDisplayId},
+        {"getBluetoothAddress", "(I)Ljava/lang/String;", (void*)nativeGetBluetoothAddress},
 };
 
 #define FIND_CLASS(var, className) \
@@ -2469,6 +2480,9 @@
 
     GET_METHOD_ID(gServiceClassInfo.notifySensorAccuracy, clazz, "notifySensorAccuracy", "(III)V");
 
+    GET_METHOD_ID(gServiceClassInfo.notifyStylusGestureStarted, clazz, "notifyStylusGestureStarted",
+                  "(IJ)V");
+
     GET_METHOD_ID(gServiceClassInfo.notifyVibratorState, clazz, "notifyVibratorState", "(IZ)V");
 
     GET_METHOD_ID(gServiceClassInfo.notifyNoFocusedWindowAnr, clazz, "notifyNoFocusedWindowAnr",
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index 91f5c69..40412db 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -21,20 +21,28 @@
 import android.annotation.NonNull;
 import android.annotation.UserIdInt;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.credentials.CreateCredentialRequest;
 import android.credentials.GetCredentialRequest;
 import android.credentials.ICreateCredentialCallback;
 import android.credentials.ICredentialManager;
 import android.credentials.IGetCredentialCallback;
+import android.os.Binder;
 import android.os.CancellationSignal;
 import android.os.ICancellationSignal;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.text.TextUtils;
 import android.util.Log;
+import android.util.Slog;
 
 import com.android.server.infra.AbstractMasterSystemService;
 import com.android.server.infra.SecureSettingsServiceNameResolver;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
 /**
  * Entry point service for credential management.
  *
@@ -49,52 +57,99 @@
 
     public CredentialManagerService(@NonNull Context context) {
         super(context,
-                new SecureSettingsServiceNameResolver(context, Settings.Secure.AUTOFILL_SERVICE),
+                new SecureSettingsServiceNameResolver(context, Settings.Secure.CREDENTIAL_SERVICE,
+                        /*isMultipleMode=*/true),
                 null, PACKAGE_UPDATE_POLICY_REFRESH_EAGER);
     }
 
     @Override
     protected String getServiceSettingsProperty() {
-        return Settings.Secure.AUTOFILL_SERVICE;
+        return Settings.Secure.CREDENTIAL_SERVICE;
     }
 
     @Override // from AbstractMasterSystemService
     protected CredentialManagerServiceImpl newServiceLocked(@UserIdInt int resolvedUserId,
             boolean disabled) {
-        return new CredentialManagerServiceImpl(this, mLock, resolvedUserId);
+        // This method should not be called for CredentialManagerService as it is configured to use
+        // multiple services.
+        Slog.w(TAG, "Should not be here - CredentialManagerService is configured to use "
+                + "multiple services");
+        return null;
     }
 
-    @Override
+    @Override // from SystemService
     public void onStart() {
-        Log.i(TAG, "onStart");
         publishBinderService(CREDENTIAL_SERVICE, new CredentialManagerServiceStub());
     }
 
+    @Override // from AbstractMasterSystemService
+    protected List<CredentialManagerServiceImpl> newServiceListLocked(int resolvedUserId,
+            boolean disabled, String[] serviceNames) {
+        if (serviceNames == null || serviceNames.length == 0) {
+            Slog.i(TAG, "serviceNames sent in newServiceListLocked is null, or empty");
+            return new ArrayList<>();
+        }
+        List<CredentialManagerServiceImpl> serviceList = new ArrayList<>(serviceNames.length);
+        for (int i = 0; i < serviceNames.length; i++) {
+            Log.i(TAG, "in newServiceListLocked, service: " + serviceNames[i]);
+            if (TextUtils.isEmpty(serviceNames[i])) {
+                continue;
+            }
+            try {
+                serviceList.add(new CredentialManagerServiceImpl(this, mLock, resolvedUserId,
+                        serviceNames[i]));
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.i(TAG, "Unable to add serviceInfo : " + e.getMessage());
+            } catch (SecurityException e) {
+                Log.i(TAG, "Unable to add serviceInfo : " + e.getMessage());
+            }
+        }
+        return serviceList;
+    }
+
+    private void runForUser(@NonNull final Consumer<CredentialManagerServiceImpl> c) {
+        final int userId = UserHandle.getCallingUserId();
+        final long origId = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                final List<CredentialManagerServiceImpl> services =
+                        getServiceListForUserLocked(userId);
+                services.forEach(s -> {
+                    c.accept(s);
+                });
+            }
+        } finally {
+            Binder.restoreCallingIdentity(origId);
+        }
+    }
+
     final class CredentialManagerServiceStub extends ICredentialManager.Stub {
         @Override
         public ICancellationSignal executeGetCredential(
                 GetCredentialRequest request,
-                IGetCredentialCallback callback) {
-            // TODO: implement.
-            Log.i(TAG, "executeGetCredential");
-
-            final int userId = UserHandle.getCallingUserId();
-            synchronized (mLock) {
-                final CredentialManagerServiceImpl service = peekServiceForUserLocked(userId);
-                if (service != null) {
-                    Log.i(TAG, "Got service for : " + userId);
-                    service.getCredential();
-                }
-            }
-
+                IGetCredentialCallback callback,
+                final String callingPackage) {
+            Log.i(TAG, "starting executeGetCredential with callingPackage: " + callingPackage);
+            // TODO : Implement cancellation
             ICancellationSignal cancelTransport = CancellationSignal.createTransport();
+
+            // New request session, scoped for this request only.
+            final GetRequestSession session = new GetRequestSession(getContext(),
+                    UserHandle.getCallingUserId(),
+                    callback);
+
+            // Invoke all services of a user
+            runForUser((service) -> {
+                service.getCredential(request, session, callingPackage);
+            });
             return cancelTransport;
         }
 
         @Override
         public ICancellationSignal executeCreateCredential(
                 CreateCredentialRequest request,
-                ICreateCredentialCallback callback) {
+                ICreateCredentialCallback callback,
+                String callingPackage) {
             // TODO: implement.
             Log.i(TAG, "executeCreateCredential");
             ICancellationSignal cancelTransport = CancellationSignal.createTransport();
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
index aa19241..cc03f9b 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
@@ -17,47 +17,94 @@
 package com.android.server.credentials;
 
 import android.annotation.NonNull;
-import android.app.AppGlobals;
+import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
-import android.os.RemoteException;
-import android.util.Log;
+import android.credentials.GetCredentialRequest;
+import android.service.credentials.CredentialProviderInfo;
+import android.service.credentials.GetCredentialsRequest;
+import android.util.Slog;
 
 import com.android.server.infra.AbstractPerUserSystemService;
 
+
 /**
- * Per-user implementation of {@link CredentialManagerService}
+ * Per-user, per remote service implementation of {@link CredentialManagerService}
  */
 public final class CredentialManagerServiceImpl extends
         AbstractPerUserSystemService<CredentialManagerServiceImpl, CredentialManagerService> {
     private static final String TAG = "CredManSysServiceImpl";
 
-    protected CredentialManagerServiceImpl(
+    // TODO(b/210531) : Make final when update flow is fixed
+    private ComponentName mRemoteServiceComponentName;
+    private CredentialProviderInfo mInfo;
+
+    public CredentialManagerServiceImpl(
             @NonNull CredentialManagerService master,
-            @NonNull Object lock, int userId) {
+            @NonNull Object lock, int userId, String serviceName)
+            throws PackageManager.NameNotFoundException {
         super(master, lock, userId);
+        Slog.i(TAG, "in CredentialManagerServiceImpl cons");
+        // TODO : Replace with newServiceInfoLocked after confirming behavior
+        mRemoteServiceComponentName = ComponentName.unflattenFromString(serviceName);
+        mInfo = new CredentialProviderInfo(getContext(), mRemoteServiceComponentName, mUserId);
     }
 
     @Override // from PerUserSystemService
     protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent)
             throws PackageManager.NameNotFoundException {
-        ServiceInfo si;
-        try {
-            si = AppGlobals.getPackageManager().getServiceInfo(serviceComponent,
-                    PackageManager.GET_META_DATA, mUserId);
-        } catch (RemoteException e) {
-            throw new PackageManager.NameNotFoundException(
-                    "Could not get service for " + serviceComponent);
-        }
-        return si;
+        // TODO : Test update flows with multiple providers
+        Slog.i(TAG , "newServiceInfoLocked with : " + serviceComponent.getPackageName());
+        mRemoteServiceComponentName = serviceComponent;
+        mInfo = new CredentialProviderInfo(getContext(), serviceComponent, mUserId);
+        return mInfo.getServiceInfo();
     }
 
-    /**
-     * Unimplemented getCredentials
-     */
-    public void getCredential() {
-        Log.i(TAG, "getCredential not implemented");
-        // TODO : Implement logic
+    public void getCredential(GetCredentialRequest request, GetRequestSession requestSession,
+            String callingPackage) {
+        Slog.i(TAG, "in getCredential in CredManServiceImpl");
+        if (mInfo == null) {
+            Slog.i(TAG, "in getCredential in CredManServiceImpl, but mInfo is null");
+            return;
+        }
+
+        // TODO : Determine if remoteService instance can be reused across requests
+        final RemoteCredentialService remoteService = new RemoteCredentialService(
+                getContext(), mInfo.getServiceInfo().getComponentName(), mUserId);
+        ProviderGetSession providerSession = new ProviderGetSession(mInfo,
+                requestSession, mUserId, remoteService);
+        // Set the provider info to the session when the request is initiated. This happens here
+        // because there is one serviceImpl per remote provider, and so we can only retrieve
+        // the provider information in the scope of this instance, whereas the session is for the
+        // entire request.
+        requestSession.addProviderSession(providerSession);
+        GetCredentialsRequest filteredRequest = getRequestWithValidType(request, callingPackage);
+        if (filteredRequest != null) {
+            remoteService.onGetCredentials(getRequestWithValidType(request, callingPackage),
+                    providerSession);
+        }
+    }
+
+    @Nullable
+    private GetCredentialsRequest getRequestWithValidType(GetCredentialRequest request,
+            String callingPackage) {
+        GetCredentialsRequest.Builder builder =
+                new GetCredentialsRequest.Builder(callingPackage);
+        request.getGetCredentialOptions().forEach( option -> {
+            if (mInfo.hasCapability(option.getType())) {
+                Slog.i(TAG, "Provider can handle: " + option.getType());
+                builder.addGetCredentialOption(option);
+            } else {
+                Slog.i(TAG, "Skipping request as provider cannot handle it");
+            }
+        });
+
+        try {
+            return builder.build();
+        } catch (IllegalArgumentException | NullPointerException e) {
+            Slog.i(TAG, "issue with request build: " + e.getMessage());
+        }
+        return null;
     }
 }
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
new file mode 100644
index 0000000..69fb1ea
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
@@ -0,0 +1,92 @@
+/*
+ * 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.credentials;
+
+import android.annotation.NonNull;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.credentials.ui.IntentFactory;
+import android.credentials.ui.ProviderData;
+import android.credentials.ui.RequestInfo;
+import android.credentials.ui.UserSelectionDialogResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.util.Log;
+import android.util.Slog;
+
+import java.util.ArrayList;
+
+/** Initiates the Credential Manager UI and receives results. */
+public class CredentialManagerUi {
+    private static final String TAG = "CredentialManagerUi";
+    @NonNull
+    private final CredentialManagerUiCallback mCallbacks;
+    @NonNull private final Context mContext;
+    private final int mUserId;
+    @NonNull private final ResultReceiver mResultReceiver = new ResultReceiver(
+            new Handler(Looper.getMainLooper())) {
+        @Override
+        protected void onReceiveResult(int resultCode, Bundle resultData) {
+            handleUiResult(resultCode, resultData);
+        }
+    };
+
+    private void handleUiResult(int resultCode, Bundle resultData) {
+        if (resultCode == Activity.RESULT_OK) {
+            UserSelectionDialogResult selection = UserSelectionDialogResult
+                    .fromResultData(resultData);
+            if (selection != null) {
+                mCallbacks.onUiSelection(selection);
+            } else {
+                Slog.i(TAG, "No selection found in UI result");
+            }
+        } else if (resultCode == Activity.RESULT_CANCELED) {
+            mCallbacks.onUiCancelation();
+        }
+    }
+
+    /**
+     * Interface to be implemented by any class that wishes to get callbacks from the UI.
+     */
+    public interface CredentialManagerUiCallback {
+        /** Called when the user makes a selection. */
+        void onUiSelection(UserSelectionDialogResult selection);
+        /** Called when the user cancels the UI. */
+        void onUiCancelation();
+    }
+    public CredentialManagerUi(Context context, int userId,
+            CredentialManagerUiCallback callbacks) {
+        Log.i(TAG, "In CredentialManagerUi constructor");
+        mContext = context;
+        mUserId = userId;
+        mCallbacks = callbacks;
+    }
+
+    /**
+     * Surfaces the Credential Manager bottom sheet UI.
+     * @param providerDataList the list of provider data from remote providers
+     */
+    public void show(RequestInfo requestInfo, ArrayList<ProviderData> providerDataList) {
+        Log.i(TAG, "In show");
+        Intent intent = IntentFactory.newIntent(requestInfo, providerDataList,
+                mResultReceiver);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(intent);
+    }
+}
diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
new file mode 100644
index 0000000..80f0fec
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
@@ -0,0 +1,164 @@
+/*
+ * 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.credentials;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.credentials.Credential;
+import android.credentials.GetCredentialResponse;
+import android.credentials.IGetCredentialCallback;
+import android.credentials.ui.ProviderData;
+import android.credentials.ui.RequestInfo;
+import android.credentials.ui.UserSelectionDialogResult;
+import android.os.RemoteException;
+import android.service.credentials.CredentialEntry;
+import android.util.Log;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Central session for a single getCredentials request. This class listens to the
+ * responses from providers, and the UX app, and updates the provider(S) state.
+ */
+public final class GetRequestSession extends RequestSession {
+    private static final String TAG = "GetRequestSession";
+
+    private final IGetCredentialCallback mClientCallback;
+    private final Map<String, ProviderGetSession> mProviders;
+
+    public GetRequestSession(Context context, int userId,
+            IGetCredentialCallback callback) {
+        super(context, userId, RequestInfo.TYPE_GET);
+        mClientCallback = callback;
+        mProviders = new HashMap<>();
+    }
+
+    /**
+     * Adds a new provider to the list of providers that are contributing to this session.
+     */
+    public void addProviderSession(ProviderGetSession providerSession) {
+        mProviders.put(providerSession.getComponentName().flattenToString(),
+                providerSession);
+    }
+
+    @Override
+    public void onProviderStatusChanged(ProviderSession.Status status,
+            ComponentName componentName) {
+        Log.i(TAG, "in onStatusChanged");
+        if (ProviderSession.isTerminatingStatus(status)) {
+            Log.i(TAG, "in onStatusChanged terminating status");
+
+            ProviderGetSession session = mProviders.remove(componentName.flattenToString());
+            if (session != null) {
+                Slog.i(TAG, "Provider session removed.");
+            } else {
+                Slog.i(TAG, "Provider session null, did not exist.");
+            }
+        } else if (ProviderSession.isCompletionStatus(status)) {
+            Log.i(TAG, "in onStatusChanged isCompletionStatus status");
+            onProviderResponseComplete();
+        }
+    }
+
+    @Override
+    public void onUiSelection(UserSelectionDialogResult selection) {
+        String providerId = selection.getProviderId();
+        ProviderGetSession providerSession = mProviders.get(providerId);
+        if (providerSession != null) {
+            CredentialEntry credentialEntry = providerSession.getCredentialEntry(
+                    selection.getEntrySubkey());
+            if (credentialEntry != null && credentialEntry.getCredential() != null) {
+                respondToClientAndFinish(credentialEntry.getCredential());
+            }
+            // TODO : Handle action chips and authentication selection
+            return;
+        }
+        // TODO : finish session and respond to client if provider not found
+    }
+
+    @Override
+    public void onUiCancelation() {
+        // User canceled the activity
+        // TODO : Send error code to client
+        finishSession();
+    }
+
+    private void onProviderResponseComplete() {
+        Log.i(TAG, "in onProviderResponseComplete");
+        if (isResponseCompleteAcrossProviders()) {
+            Log.i(TAG, "in onProviderResponseComplete - isResponseCompleteAcrossProviders");
+            getProviderDataAndInitiateUi();
+        }
+    }
+
+    private void getProviderDataAndInitiateUi() {
+        ArrayList<ProviderData> providerDataList = new ArrayList<>();
+        for (ProviderGetSession session : mProviders.values()) {
+            Log.i(TAG, "preparing data for : " + session.getComponentName());
+            providerDataList.add(session.prepareUiData());
+        }
+        if (!providerDataList.isEmpty()) {
+            Log.i(TAG, "provider list not empty about to initiate ui");
+            initiateUi(providerDataList);
+        }
+    }
+
+    private void initiateUi(ArrayList<ProviderData> providerDataList) {
+        mHandler.post(() -> mCredentialManagerUi.show(RequestInfo.newGetRequestInfo(
+                mRequestId, null, mIsFirstUiTurn, ""),
+                providerDataList));
+    }
+
+    /**
+     * Iterates over all provider sessions and returns true if all have responded.
+     */
+    private boolean isResponseCompleteAcrossProviders() {
+        AtomicBoolean isRequestComplete = new AtomicBoolean(true);
+        mProviders.forEach( (packageName, session) -> {
+            if (session.getStatus() != ProviderSession.Status.COMPLETE) {
+                isRequestComplete.set(false);
+            }
+        });
+        return isRequestComplete.get();
+    }
+
+    private void respondToClientAndFinish(Credential credential) {
+        try {
+            mClientCallback.onResponse(new GetCredentialResponse(credential));
+        } catch (RemoteException e) {
+            e.printStackTrace();
+        }
+        finishSession();
+    }
+
+    private void finishSession() {
+        clearProviderSessions();
+    }
+
+    private void clearProviderSessions() {
+        for (ProviderGetSession session : mProviders.values()) {
+            // TODO : Evaluate if we should unbind remote services here or wait for them
+            // to automatically unbind when idle. Re-binding frequently also has a cost.
+            //session.destroy();
+        }
+        mProviders.clear();
+    }
+}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
new file mode 100644
index 0000000..24610df
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
@@ -0,0 +1,197 @@
+/*
+ * 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.credentials;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.slice.Slice;
+import android.credentials.ui.Entry;
+import android.credentials.ui.ProviderData;
+import android.service.credentials.Action;
+import android.service.credentials.CredentialEntry;
+import android.service.credentials.CredentialProviderInfo;
+import android.service.credentials.CredentialsDisplayContent;
+import android.service.credentials.GetCredentialsResponse;
+import android.util.Log;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Central provider session that listens for provider callbacks, and maintains provider state.
+ * Will likely split this into remote response state and UI state.
+ */
+public final class ProviderGetSession extends ProviderSession<GetCredentialsResponse>
+        implements RemoteCredentialService.ProviderCallbacks<GetCredentialsResponse> {
+    private static final String TAG = "ProviderGetSession";
+
+    // Key to be used as an entry key for a credential entry
+    private static final String CREDENTIAL_ENTRY_KEY = "credential_key";
+
+    private GetCredentialsResponse mResponse;
+
+    @NonNull
+    private final Map<String, CredentialEntry> mUiCredentials = new HashMap<>();
+
+    @NonNull
+    private final Map<String, Action> mUiActions = new HashMap<>();
+
+    public ProviderGetSession(CredentialProviderInfo info,
+            ProviderInternalCallback callbacks,
+            int userId, RemoteCredentialService remoteCredentialService) {
+        super(info, callbacks, userId, remoteCredentialService);
+        setStatus(Status.PENDING);
+    }
+
+    /** Updates the response being maintained in state by this provider session. */
+    @Override
+    public void updateResponse(GetCredentialsResponse response) {
+        if (response.getAuthenticationAction() != null) {
+            // TODO : Implement authentication logic
+        } else if (response.getCredentialsDisplayContent() != null) {
+            Log.i(TAG , "updateResponse with credentialEntries");
+            mResponse = response;
+            updateStatusAndInvokeCallback(Status.COMPLETE);
+        }
+    }
+
+    /** Returns the response being maintained in this provider session. */
+    @Override
+    @Nullable
+    public GetCredentialsResponse getResponse() {
+        return  mResponse;
+    }
+
+    /** Returns the credential entry maintained in state by this provider session. */
+    @Nullable
+    public CredentialEntry getCredentialEntry(@NonNull String entryId) {
+        return mUiCredentials.get(entryId);
+    }
+
+    /** Returns the action entry maintained in state by this provider session. */
+    @Nullable
+    public Action getAction(@NonNull String entryId) {
+        return mUiActions.get(entryId);
+    }
+
+    /** Called when the provider response has been updated by an external source. */
+    @Override
+    public void onProviderResponseSuccess(@Nullable GetCredentialsResponse response) {
+        Log.i(TAG, "in onProviderResponseSuccess");
+        updateResponse(response);
+    }
+
+    /** Called when the provider response resulted in a failure. */
+    @Override
+    public void onProviderResponseFailure(int errorCode, @Nullable CharSequence message) {
+        updateStatusAndInvokeCallback(toStatus(errorCode));
+    }
+
+    /** Called when provider service dies. */
+    @Override
+    public void onProviderServiceDied(RemoteCredentialService service) {
+        if (service.getComponentName().equals(mProviderInfo.getServiceInfo().getComponentName())) {
+            updateStatusAndInvokeCallback(Status.SERVICE_DEAD);
+        } else {
+            Slog.i(TAG, "Component names different in onProviderServiceDied - "
+                    + "this should not happen");
+        }
+    }
+
+    @Override
+    protected final ProviderData prepareUiData() throws IllegalArgumentException {
+        Log.i(TAG, "In prepareUiData");
+        if (!ProviderSession.isCompletionStatus(getStatus())) {
+            Log.i(TAG, "In prepareUiData not complete");
+
+            throw new IllegalStateException("Status must be in completion mode");
+        }
+        GetCredentialsResponse response = getResponse();
+        if (response == null) {
+            Log.i(TAG, "In prepareUiData response null");
+
+            throw new IllegalStateException("Response must be in completion mode");
+        }
+        if (response.getAuthenticationAction() != null) {
+            Log.i(TAG, "In prepareUiData auth not null");
+
+            return prepareUiProviderDataWithAuthentication(response.getAuthenticationAction());
+        }
+        if (response.getCredentialsDisplayContent() != null){
+            Log.i(TAG, "In prepareUiData credentials not null");
+
+            return prepareUiProviderDataWithCredentials(response.getCredentialsDisplayContent());
+        }
+        return null;
+    }
+
+    /**
+     * To be called by {@link ProviderGetSession} when the UI is to be invoked.
+     */
+    @Nullable
+    private ProviderData prepareUiProviderDataWithCredentials(@NonNull
+            CredentialsDisplayContent content) {
+        Log.i(TAG, "in prepareUiProviderData");
+        List<Entry> credentialEntries = new ArrayList<>();
+        List<Entry> actionChips = new ArrayList<>();
+        Entry authenticationEntry = null;
+
+        // Populate the credential entries
+        for (CredentialEntry credentialEntry : content.getCredentialEntries()) {
+            String entryId = UUID.randomUUID().toString();
+            mUiCredentials.put(entryId, credentialEntry);
+            Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId);
+            Slice slice = credentialEntry.getSlice();
+            // TODO : Remove conversion of string to int after change in Entry class
+            credentialEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId,
+                    credentialEntry.getSlice()));
+        }
+        // populate the action chip
+        for (Action action : content.getActions()) {
+            String entryId = UUID.randomUUID().toString();
+            mUiActions.put(entryId, action);
+            // TODO : Remove conversion of string to int after change in Entry class
+            actionChips.add(new Entry(ACTION_ENTRY_KEY, entryId,
+                    action.getSlice()));
+        }
+
+        // TODO : Set the correct last used time
+        return new ProviderData.Builder(mComponentName.flattenToString(),
+                mProviderInfo.getServiceLabel() == null ? "" :
+                        mProviderInfo.getServiceLabel().toString(),
+                /*icon=*/null)
+                .setCredentialEntries(credentialEntries)
+                .setActionChips(actionChips)
+                .setAuthenticationEntry(authenticationEntry)
+                .setLastUsedTimeMillis(0)
+                .build();
+    }
+
+    /**
+     * To be called by {@link ProviderGetSession} when the UI is to be invoked.
+     */
+    @Nullable
+    private ProviderData prepareUiProviderDataWithAuthentication(@NonNull
+            Action authenticationEntry) {
+        // TODO : Implement authentication flow
+        return null;
+    }
+}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java
new file mode 100644
index 0000000..3a9f964
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java
@@ -0,0 +1,124 @@
+/*
+ * 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.credentials;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.credentials.ui.ProviderData;
+import android.service.credentials.CredentialProviderException;
+import android.service.credentials.CredentialProviderInfo;
+
+/**
+ * Provider session storing the state of provider response and ui entries.
+ * @param <T> The request type expected from the remote provider, for a given request session.
+ */
+public abstract class ProviderSession<T> implements RemoteCredentialService.ProviderCallbacks<T> {
+    // Key to be used as the entry key for an action entry
+    protected static final String ACTION_ENTRY_KEY = "action_key";
+
+    @NonNull protected final ComponentName mComponentName;
+    @NonNull protected final CredentialProviderInfo mProviderInfo;
+    @NonNull protected final RemoteCredentialService mRemoteCredentialService;
+    @NonNull protected final int mUserId;
+    @NonNull protected Status mStatus = Status.NOT_STARTED;
+    @NonNull protected final ProviderInternalCallback mCallbacks;
+
+    /**
+     * Interface to be implemented by any class that wishes to get a callback when a particular
+     * provider session's status changes. Typically, implemented by the {@link RequestSession}
+     * class.
+     */
+    public interface ProviderInternalCallback {
+        /**
+         * Called when status changes.
+         */
+        void onProviderStatusChanged(Status status, ComponentName componentName);
+    }
+
+    protected ProviderSession(@NonNull CredentialProviderInfo info,
+            @NonNull ProviderInternalCallback callbacks,
+            @NonNull int userId,
+            @NonNull RemoteCredentialService remoteCredentialService) {
+        mProviderInfo = info;
+        mCallbacks = callbacks;
+        mUserId = userId;
+        mComponentName = info.getServiceInfo().getComponentName();
+        mRemoteCredentialService = remoteCredentialService;
+    }
+
+    /** Update the response state stored with the provider session. */
+    protected abstract void updateResponse (T response);
+
+    /** Update the response state stored with the provider session. */
+    protected abstract T getResponse ();
+
+    /** Should be overridden to prepare, and stores state for {@link ProviderData} to be
+     * shown on the UI. */
+    protected abstract ProviderData prepareUiData();
+
+    /** Provider status at various states of the request session. */
+    enum Status {
+        NOT_STARTED,
+        PENDING,
+        REQUIRES_AUTHENTICATION,
+        COMPLETE,
+        SERVICE_DEAD,
+        CANCELED
+    }
+
+    protected void setStatus(@NonNull Status status) {
+        mStatus = status;
+    }
+
+    @NonNull
+    protected Status getStatus() {
+        return mStatus;
+    }
+
+    @NonNull
+    protected ComponentName getComponentName() {
+        return mComponentName;
+    }
+
+    /** Updates the status .*/
+    protected void updateStatusAndInvokeCallback(@NonNull Status status) {
+        setStatus(status);
+        mCallbacks.onProviderStatusChanged(status, mComponentName);
+    }
+
+    @NonNull
+    public static Status toStatus(
+            @CredentialProviderException.CredentialProviderError int errorCode) {
+        // TODO : Add more mappings as more flows are supported
+        return Status.CANCELED;
+    }
+
+    /**
+     * Returns true if the given status means that the provider session must be terminated.
+     */
+    public static boolean isTerminatingStatus(Status status) {
+        return status == Status.CANCELED || status == Status.SERVICE_DEAD;
+    }
+
+    /**
+     * Returns true if the given status means that the provider is done getting the response,
+     * and is ready for user interaction.
+     */
+    public static boolean isCompletionStatus(Status status) {
+        return status == Status.COMPLETE || status == Status.REQUIRES_AUTHENTICATION;
+    }
+}
diff --git a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java
new file mode 100644
index 0000000..d0b6e7d
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java
@@ -0,0 +1,185 @@
+/*
+ * 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.credentials;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.ICancellationSignal;
+import android.os.RemoteException;
+import android.service.credentials.CredentialProviderException;
+import android.service.credentials.CredentialProviderException.CredentialProviderError;
+import android.service.credentials.CredentialProviderService;
+import android.service.credentials.GetCredentialsRequest;
+import android.service.credentials.GetCredentialsResponse;
+import android.service.credentials.ICredentialProviderService;
+import android.service.credentials.IGetCredentialsCallback;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.internal.infra.ServiceConnector;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Handles connections with the remote credential provider
+ *
+ * @hide
+ */
+public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialProviderService>{
+
+    private static final String TAG = "RemoteCredentialService";
+    /** Timeout for a single request. */
+    private static final long TIMEOUT_REQUEST_MILLIS = 5 * DateUtils.SECOND_IN_MILLIS;
+    /** Timeout to unbind after the task queue is empty. */
+    private static final long TIMEOUT_IDLE_SERVICE_CONNECTION_MILLIS =
+            5 * DateUtils.SECOND_IN_MILLIS;
+
+    private final ComponentName mComponentName;
+
+    /**
+     * Callbacks to be invoked when the provider remote service responds with a
+     * success or failure.
+     * @param <T> the type of response expected from the provider
+     */
+    public interface ProviderCallbacks<T> {
+        /** Called when a successful response is received from the remote provider. */
+        void onProviderResponseSuccess(@Nullable T response);
+        /** Called when a failure response is received from the remote provider. */
+        void onProviderResponseFailure(int errorCode, @Nullable CharSequence message);
+        /** Called when the remote provider service dies. */
+        void onProviderServiceDied(RemoteCredentialService service);
+    }
+
+    public RemoteCredentialService(@NonNull Context context,
+            @NonNull ComponentName componentName, int userId) {
+        super(context, new Intent(CredentialProviderService.SERVICE_INTERFACE)
+                        .setComponent(componentName), Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS,
+                userId, ICredentialProviderService.Stub::asInterface);
+        mComponentName = componentName;
+    }
+
+    /** Unbinds automatically after this amount of time. */
+    @Override
+    protected long getAutoDisconnectTimeoutMs() {
+        return TIMEOUT_IDLE_SERVICE_CONNECTION_MILLIS;
+    }
+
+    /** Return the componentName of the service to be connected. */
+    @NonNull public ComponentName getComponentName() {
+        return mComponentName;
+    }
+
+    /** Destroys this remote service by unbinding the connection. */
+    public void destroy() {
+        unbind();
+    }
+
+    /** Main entry point to be called for executing a getCredential call on the remote
+     * provider service.
+     * @param request the request to be sent to the provider
+     * @param callback the callback to be used to send back the provider response to the
+     *                 {@link ProviderSession} class that maintains provider state
+     */
+    public void onGetCredentials(@NonNull GetCredentialsRequest request,
+            ProviderCallbacks<GetCredentialsResponse> callback) {
+        Log.i(TAG, "In onGetCredentials in RemoteCredentialService");
+        AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>();
+        AtomicReference<CompletableFuture<GetCredentialsResponse>> futureRef =
+                new AtomicReference<>();
+
+        CompletableFuture<GetCredentialsResponse> connectThenExecute = postAsync(service -> {
+            CompletableFuture<GetCredentialsResponse> getCredentials = new CompletableFuture<>();
+            ICancellationSignal cancellationSignal =
+                    service.onGetCredentials(request, new IGetCredentialsCallback.Stub() {
+                @Override
+                public void onSuccess(GetCredentialsResponse response) {
+                    Log.i(TAG, "In onSuccess in RemoteCredentialService");
+                    getCredentials.complete(response);
+                }
+
+                @Override
+                public void onFailure(@CredentialProviderError int errorCode,
+                        CharSequence message) {
+                    Log.i(TAG, "In onFailure in RemoteCredentialService");
+                    String errorMsg = message == null ? "" : String.valueOf(message);
+                    getCredentials.completeExceptionally(new CredentialProviderException(
+                            errorCode, errorMsg));
+                }
+            });
+            CompletableFuture<GetCredentialsResponse> future = futureRef.get();
+            if (future != null && future.isCancelled()) {
+                dispatchCancellationSignal(cancellationSignal);
+            } else {
+                cancellationSink.set(cancellationSignal);
+            }
+            return getCredentials;
+        }).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS);
+        futureRef.set(connectThenExecute);
+
+        connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() -> {
+            if (error == null) {
+                Log.i(TAG, "In RemoteCredentialService execute error is null");
+                callback.onProviderResponseSuccess(result);
+            } else {
+                if (error instanceof TimeoutException) {
+                    Log.i(TAG, "In RemoteCredentialService execute error is timeout");
+                    dispatchCancellationSignal(cancellationSink.get());
+                    callback.onProviderResponseFailure(
+                            CredentialProviderException.ERROR_TIMEOUT,
+                            error.getMessage());
+                } else if (error instanceof CancellationException) {
+                    Log.i(TAG, "In RemoteCredentialService execute error is cancellation");
+                    dispatchCancellationSignal(cancellationSink.get());
+                    callback.onProviderResponseFailure(
+                            CredentialProviderException.ERROR_TASK_CANCELED,
+                            error.getMessage());
+                } else if (error instanceof CredentialProviderException) {
+                    Log.i(TAG, "In RemoteCredentialService execute error is provider error");
+                    callback.onProviderResponseFailure(((CredentialProviderException) error)
+                                    .getErrorCode(),
+                            error.getMessage());
+                } else {
+                    Log.i(TAG, "In RemoteCredentialService execute error is unknown");
+                    callback.onProviderResponseFailure(
+                            CredentialProviderException.ERROR_UNKNOWN,
+                            error.getMessage());
+                }
+            }
+        }));
+    }
+
+    private void dispatchCancellationSignal(@Nullable ICancellationSignal signal) {
+        if (signal == null) {
+            Slog.e(TAG, "Error dispatching a cancellation - Signal is null");
+            return;
+        }
+        try {
+            signal.cancel();
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Error dispatching a cancellation", e);
+        }
+    }
+}
diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java
new file mode 100644
index 0000000..1bacbb3
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/RequestSession.java
@@ -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.android.server.credentials;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.content.ComponentName;
+import android.content.Context;
+import android.credentials.ui.UserSelectionDialogResult;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+
+/**
+ * Base class of a request session, that listens to UI events. This class must be extended
+ * every time a new response type is expected from the providers.
+ */
+abstract class RequestSession implements CredentialManagerUi.CredentialManagerUiCallback,
+        ProviderSession.ProviderInternalCallback {
+    @NonNull protected final IBinder mRequestId;
+    @NonNull protected final Context mContext;
+    @NonNull protected final CredentialManagerUi mCredentialManagerUi;
+    @NonNull protected final String mRequestType;
+    @NonNull protected final Handler mHandler;
+    @NonNull protected boolean mIsFirstUiTurn = true;
+    @UserIdInt protected final int mUserId;
+
+    protected RequestSession(@NonNull Context context,
+            @UserIdInt int userId, @NonNull String requestType) {
+        mContext = context;
+        mUserId = userId;
+        mRequestType = requestType;
+        mHandler = new Handler(Looper.getMainLooper(), null, true);
+        mRequestId = new Binder();
+        mCredentialManagerUi = new CredentialManagerUi(mContext,
+                mUserId, this);
+    }
+
+    /** Returns the unique identifier of this request session. */
+    public IBinder getRequestId() {
+        return mRequestId;
+    }
+
+    @Override // from CredentialManagerUiCallback
+    public abstract void onUiSelection(UserSelectionDialogResult selection);
+
+    @Override // from CredentialManagerUiCallback
+    public abstract void onUiCancelation();
+
+    @Override // from ProviderInternalCallback
+    public abstract void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName);
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index b74fedf..593e648 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -154,6 +154,7 @@
 import com.android.server.people.PeopleService;
 import com.android.server.pm.ApexManager;
 import com.android.server.pm.ApexSystemServiceInfo;
+import com.android.server.pm.BackgroundInstallControlService;
 import com.android.server.pm.CrossProfileAppsService;
 import com.android.server.pm.DataLoaderManagerService;
 import com.android.server.pm.DynamicCodeLoggingService;
@@ -2469,6 +2470,10 @@
             t.traceBegin("StartMediaMetricsManager");
             mSystemServiceManager.startService(MediaMetricsManagerService.class);
             t.traceEnd();
+
+            t.traceBegin("StartBackgroundInstallControlService");
+            mSystemServiceManager.startService(BackgroundInstallControlService.class);
+            t.traceEnd();
         }
 
         t.traceBegin("StartMediaProjectionManager");
diff --git a/services/print/java/com/android/server/print/PrintManagerService.java b/services/print/java/com/android/server/print/PrintManagerService.java
index 66524edf..35b9bc3 100644
--- a/services/print/java/com/android/server/print/PrintManagerService.java
+++ b/services/print/java/com/android/server/print/PrintManagerService.java
@@ -984,6 +984,7 @@
             monitor.register(mContext, BackgroundThread.getHandler().getLooper(),
                     UserHandle.ALL, true);
         }
+
         private UserState getOrCreateUserStateLocked(int userId, boolean lowPriority) {
             return getOrCreateUserStateLocked(userId, lowPriority,
                     true /* enforceUserUnlockingOrUnlocked */);
@@ -991,6 +992,12 @@
 
         private UserState getOrCreateUserStateLocked(int userId, boolean lowPriority,
                 boolean enforceUserUnlockingOrUnlocked) {
+            return getOrCreateUserStateLocked(userId, lowPriority,
+                    enforceUserUnlockingOrUnlocked, false /* shouldUpdateState */);
+        }
+
+        private UserState getOrCreateUserStateLocked(int userId, boolean lowPriority,
+                boolean enforceUserUnlockingOrUnlocked, boolean shouldUpdateState) {
             if (enforceUserUnlockingOrUnlocked && !mUserManager.isUserUnlockingOrUnlocked(userId)) {
                 throw new IllegalStateException(
                         "User " + userId + " must be unlocked for printing to be available");
@@ -1000,6 +1007,8 @@
             if (userState == null) {
                 userState = new UserState(mContext, userId, mLock, lowPriority);
                 mUserStates.put(userId, userState);
+            } else if (shouldUpdateState) {
+                userState.updateIfNeededLocked();
             }
 
             if (!lowPriority) {
@@ -1019,9 +1028,9 @@
 
                     UserState userState;
                     synchronized (mLock) {
-                        userState = getOrCreateUserStateLocked(userId, true,
-                                false /*enforceUserUnlockingOrUnlocked */);
-                        userState.updateIfNeededLocked();
+                        userState = getOrCreateUserStateLocked(userId, /* lowPriority */ true,
+                                /* enforceUserUnlockingOrUnlocked */ false,
+                                /* shouldUpdateState */ true);
                     }
                     // This is the first time we switch to this user after boot, so
                     // now is the time to remove obsolete print jobs since they
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java
index f46877e..2547347 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java
@@ -16,10 +16,19 @@
 
 package com.android.server.job;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.server.job.JobConcurrencyManager.KEY_PKG_CONCURRENCY_LIMIT_EJ;
 import static com.android.server.job.JobConcurrencyManager.KEY_PKG_CONCURRENCY_LIMIT_REGULAR;
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BG;
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BGUSER;
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BGUSER_IMPORTANT;
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_EJ;
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_FGS;
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_TOP;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
@@ -47,15 +56,6 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BG;
-import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BGUSER;
-import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BGUSER_IMPORTANT;
-import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_EJ;
-import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_FGS;
-import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
-import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_TOP;
-
 import com.android.internal.R;
 import com.android.internal.app.IBatteryStats;
 import com.android.server.LocalServices;
@@ -73,6 +73,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoSession;
 import org.mockito.quality.Strictness;
+import org.mockito.stubbing.Answer;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -124,6 +125,7 @@
         mMockingSession = mockitoSession()
                 .initMocks(this)
                 .mockStatic(AppGlobals.class)
+                .spyStatic(DeviceConfig.class)
                 .strictness(Strictness.LENIENT)
                 .startMocking();
         final JobSchedulerService jobSchedulerService = mock(JobSchedulerService.class);
@@ -134,6 +136,8 @@
         when(mContext.getResources()).thenReturn(mResources);
         doReturn(mContext).when(jobSchedulerService).getTestableContext();
         mConfigBuilder = new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER);
+        doAnswer((Answer<DeviceConfig.Properties>) invocationOnMock -> mConfigBuilder.build())
+                .when(() -> DeviceConfig.getProperties(eq(DeviceConfig.NAMESPACE_JOB_SCHEDULER)));
         mPendingJobQueue = new PendingJobQueue();
         doReturn(mPendingJobQueue).when(jobSchedulerService).getPendingJobQueue();
         doReturn(mIPackageManager).when(AppGlobals::getPackageManager);
@@ -595,7 +599,6 @@
     }
 
     private void updateDeviceConfig() throws Exception {
-        DeviceConfig.setProperties(mConfigBuilder.build());
         mJobConcurrencyManager.updateConfigLocked();
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
index aa95916..f88e18b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
@@ -184,8 +184,10 @@
         when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunning)).thenReturn(true);
         when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunning))
                 .thenReturn(true);
-        when(mJobSchedulerService.isLongRunningLocked(jobLowPriorityRunningLong)).thenReturn(true);
-        when(mJobSchedulerService.isLongRunningLocked(jobHighPriorityRunningLong)).thenReturn(true);
+        when(mJobSchedulerService.isJobInOvertimeLocked(jobLowPriorityRunningLong))
+                .thenReturn(true);
+        when(mJobSchedulerService.isJobInOvertimeLocked(jobHighPriorityRunningLong))
+                .thenReturn(true);
 
         assertFalse(mThermalStatusRestriction.isJobRestricted(jobMinPriority));
         assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriority));
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerInternalTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerInternalTest.java
deleted file mode 100644
index 278e04a..0000000
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerInternalTest.java
+++ /dev/null
@@ -1,258 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.server.pm;
-
-import static android.os.UserHandle.USER_SYSTEM;
-import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.Display.INVALID_DISPLAY;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static org.junit.Assert.assertThrows;
-
-import android.util.Log;
-
-import org.junit.Test;
-
-/**
- * Run as {@code atest FrameworksMockingServicesTests:com.android.server.pm.UserManagerInternalTest}
- */
-public final class UserManagerInternalTest extends UserManagerServiceOrInternalTestCase {
-
-    private static final String TAG = UserManagerInternalTest.class.getSimpleName();
-
-    // NOTE: most the tests below only apply to MUMD configurations, so we're not adding _mumd_
-    // in the test names, but _nonMumd_ instead
-
-    @Test
-    public void testAssignUserToDisplay_nonMumd_defaultDisplayIgnored() {
-        mUmi.assignUserToDisplay(USER_ID, DEFAULT_DISPLAY);
-
-        assertNoUserAssignedToDisplay();
-    }
-
-    @Test
-    public void testAssignUserToDisplay_nonMumd_otherDisplay_currentUser() {
-        mockCurrentUser(USER_ID);
-
-        assertThrows(UnsupportedOperationException.class,
-                () -> mUmi.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID));
-    }
-
-    @Test
-    public void testAssignUserToDisplay_nonMumd_otherDisplay_startProfileOfcurrentUser() {
-        mockCurrentUser(PARENT_USER_ID);
-        addDefaultProfileAndParent();
-        startDefaultProfile();
-
-        assertThrows(UnsupportedOperationException.class,
-                () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
-    }
-
-    @Test
-    public void testAssignUserToDisplay_nonMumd_otherDisplay_stoppedProfileOfcurrentUser() {
-        mockCurrentUser(PARENT_USER_ID);
-        addDefaultProfileAndParent();
-        stopDefaultProfile();
-
-        assertThrows(UnsupportedOperationException.class,
-                () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
-    }
-
-    @Test
-    public void testAssignUserToDisplay_defaultDisplayIgnored() {
-        enableUsersOnSecondaryDisplays();
-
-        mUmi.assignUserToDisplay(USER_ID, DEFAULT_DISPLAY);
-
-        assertNoUserAssignedToDisplay();
-    }
-
-    @Test
-    public void testAssignUserToDisplay_systemUser() {
-        enableUsersOnSecondaryDisplays();
-
-        assertThrows(IllegalArgumentException.class,
-                () -> mUmi.assignUserToDisplay(USER_SYSTEM, SECONDARY_DISPLAY_ID));
-    }
-
-    @Test
-    public void testAssignUserToDisplay_invalidDisplay() {
-        enableUsersOnSecondaryDisplays();
-
-        assertThrows(IllegalArgumentException.class,
-                () -> mUmi.assignUserToDisplay(USER_ID, INVALID_DISPLAY));
-    }
-
-    @Test
-    public void testAssignUserToDisplay_currentUser() {
-        enableUsersOnSecondaryDisplays();
-        mockCurrentUser(USER_ID);
-
-        assertThrows(IllegalArgumentException.class,
-                () -> mUmi.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID));
-
-        assertNoUserAssignedToDisplay();
-    }
-
-    @Test
-    public void testAssignUserToDisplay_startedProfileOfCurrentUser() {
-        enableUsersOnSecondaryDisplays();
-        mockCurrentUser(PARENT_USER_ID);
-        addDefaultProfileAndParent();
-        startDefaultProfile();
-
-        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
-                () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
-
-        Log.v(TAG, "Exception: " + e);
-        assertNoUserAssignedToDisplay();
-    }
-
-    @Test
-    public void testAssignUserToDisplay_stoppedProfileOfCurrentUser() {
-        enableUsersOnSecondaryDisplays();
-        mockCurrentUser(PARENT_USER_ID);
-        addDefaultProfileAndParent();
-        stopDefaultProfile();
-
-        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
-                () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
-
-        Log.v(TAG, "Exception: " + e);
-        assertNoUserAssignedToDisplay();
-    }
-
-    @Test
-    public void testAssignUserToDisplay_displayAvailable() {
-        enableUsersOnSecondaryDisplays();
-
-        mUmi.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
-
-        assertUserAssignedToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
-    }
-
-    @Test
-    public void testAssignUserToDisplay_displayAlreadyAssigned() {
-        enableUsersOnSecondaryDisplays();
-
-        mUmi.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
-
-        IllegalStateException e = assertThrows(IllegalStateException.class,
-                () -> mUmi.assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID));
-
-        Log.v(TAG, "Exception: " + e);
-        assertWithMessage("exception (%s) message", e).that(e).hasMessageThat()
-                .matches("Cannot.*" + OTHER_USER_ID + ".*" + SECONDARY_DISPLAY_ID + ".*already.*"
-                        + USER_ID + ".*");
-    }
-
-    @Test
-    public void testAssignUserToDisplay_userAlreadyAssigned() {
-        enableUsersOnSecondaryDisplays();
-
-        mUmi.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
-
-        IllegalStateException e = assertThrows(IllegalStateException.class,
-                () -> mUmi.assignUserToDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID));
-
-        Log.v(TAG, "Exception: " + e);
-        assertWithMessage("exception (%s) message", e).that(e).hasMessageThat()
-                .matches("Cannot.*" + USER_ID + ".*" + OTHER_SECONDARY_DISPLAY_ID + ".*already.*"
-                        + SECONDARY_DISPLAY_ID + ".*");
-
-        assertUserAssignedToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
-    }
-
-    @Test
-    public void testAssignUserToDisplay_profileOnSameDisplayAsParent() {
-        enableUsersOnSecondaryDisplays();
-        addDefaultProfileAndParent();
-
-        mUmi.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
-        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
-                () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
-
-        Log.v(TAG, "Exception: " + e);
-        assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
-    }
-
-    @Test
-    public void testAssignUserToDisplay_profileOnDifferentDisplayAsParent() {
-        enableUsersOnSecondaryDisplays();
-        addDefaultProfileAndParent();
-
-        mUmi.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
-        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
-                () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, OTHER_SECONDARY_DISPLAY_ID));
-
-        Log.v(TAG, "Exception: " + e);
-        assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
-    }
-
-    @Test
-    public void testAssignUserToDisplay_profileDefaultDisplayParentOnSecondaryDisplay() {
-        enableUsersOnSecondaryDisplays();
-        addDefaultProfileAndParent();
-
-        mUmi.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
-        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
-                () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY));
-
-        Log.v(TAG, "Exception: " + e);
-        assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
-    }
-
-    @Test
-    public void testUnassignUserFromDisplay_nonMumd_ignored() {
-        mockCurrentUser(USER_ID);
-
-        mUmi.unassignUserFromDisplay(USER_SYSTEM);
-        mUmi.unassignUserFromDisplay(USER_ID);
-        mUmi.unassignUserFromDisplay(OTHER_USER_ID);
-
-        assertNoUserAssignedToDisplay();
-    }
-
-    @Test
-    public void testUnassignUserFromDisplay() {
-        testAssignUserToDisplay_displayAvailable();
-
-        mUmi.unassignUserFromDisplay(USER_ID);
-
-        assertNoUserAssignedToDisplay();
-    }
-
-    @Override
-    protected boolean isUserVisible(int userId) {
-        return mUmi.isUserVisible(userId);
-    }
-
-    @Override
-    protected boolean isUserVisibleOnDisplay(int userId, int displayId) {
-        return mUmi.isUserVisible(userId, displayId);
-    }
-
-    @Override
-    protected int getDisplayAssignedToUser(int userId) {
-        return mUmi.getDisplayAssignedToUser(userId);
-    }
-
-    @Override
-    protected int getUserAssignedToDisplay(int displayId) {
-        return mUmi.getUserAssignedToDisplay(displayId);
-    }
-}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceOrInternalTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceOrInternalTestCase.java
index 90a5fa0..6c85b7a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceOrInternalTestCase.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceOrInternalTestCase.java
@@ -15,10 +15,6 @@
  */
 package com.android.server.pm;
 
-import static android.os.UserHandle.USER_NULL;
-import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.Display.INVALID_DISPLAY;
-
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 
@@ -34,7 +30,6 @@
 import android.os.UserManager;
 import android.util.Log;
 import android.util.SparseArray;
-import android.util.SparseIntArray;
 
 import androidx.test.annotation.UiThreadTest;
 
@@ -46,12 +41,8 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Test;
 import org.mockito.Mock;
 
-import java.util.LinkedHashMap;
-import java.util.Map;
-
 /**
  * Base class for {@link UserManagerInternalTest} and {@link UserManagerInternalTest}.
  *
@@ -59,9 +50,13 @@
  * "symbiotic relationship - some methods of the former simply call the latter and vice versa.
  *
  * <p>Ideally, only one of them should have the logic, but since that's not the case, this class
- * provices the infra to make it easier to test both (which in turn would make it easier / safer to
+ * provides the infra to make it easier to test both (which in turn would make it easier / safer to
  * refactor their logic later).
  */
+// TODO(b/244644281): there is no UserManagerInternalTest anymore as the logic being tested there
+// moved to UserVisibilityController, so it might be simpler to merge this class into
+// UserManagerServiceTest (once the UserVisibilityController -> UserManagerService dependency is
+// fixed)
 abstract class UserManagerServiceOrInternalTestCase extends ExtendedMockitoTestCase {
 
     private static final String TAG = UserManagerServiceOrInternalTestCase.class.getSimpleName();
@@ -90,31 +85,12 @@
      */
     protected static final int PROFILE_USER_ID = 643;
 
-    /**
-     * Id of a secondary display (i.e, not {@link android.view.Display.DEFAULT_DISPLAY}).
-     */
-    protected static final int SECONDARY_DISPLAY_ID = 42;
-
-    /**
-     * Id of another secondary display (i.e, not {@link android.view.Display.DEFAULT_DISPLAY}).
-     */
-    protected static final int OTHER_SECONDARY_DISPLAY_ID = 108;
-
     private final Object mPackagesLock = new Object();
     private final Context mRealContext = androidx.test.InstrumentationRegistry.getInstrumentation()
             .getTargetContext();
     private final SparseArray<UserData> mUsers = new SparseArray<>();
 
-    // TODO(b/244644281): manipulating mUsersOnSecondaryDisplays directly leaks implementation
-    // details into the unit test, but it's fine for now - in the long term, this logic should be
-    // refactored into a proper UserDisplayAssignment class.
-    private final SparseIntArray mUsersOnSecondaryDisplays = new SparseIntArray();
-
     private Context mSpiedContext;
-    private UserManagerService mStandardUms;
-    private UserManagerService mMumdUms;
-    private UserManagerInternal mStandardUmi;
-    private UserManagerInternal mMumdUmi;
 
     private @Mock PackageManagerService mMockPms;
     private @Mock UserDataPreparer mMockUserDataPreparer;
@@ -122,17 +98,11 @@
 
     /**
      * Reference to the {@link UserManagerService} being tested.
-     *
-     * <p>By default, such service doesn't support {@code MUMD} (Multiple Users on Multiple
-     * Displays), but that can be changed by calling {@link #enableUsersOnSecondaryDisplays()}.
      */
     protected UserManagerService mUms;
 
     /**
      * Reference to the {@link UserManagerInternal} being tested.
-     *
-     * <p>By default, such service doesn't support {@code MUMD} (Multiple Users on Multiple
-     * Displays), but that can be changed by calling {@link #enableUsersOnSecondaryDisplays()}.
      */
     protected UserManagerInternal mUmi;
 
@@ -151,32 +121,12 @@
         // Called when WatchedUserStates is constructed
         doNothing().when(() -> UserManager.invalidateIsUserUnlockedCache());
 
-        // Need to set both UserManagerService instances here, as they need to be run in the
-        // UiThread
-
-        // mMumdUms / mMumdUmi
-        mockIsUsersOnSecondaryDisplaysEnabled(/* usersOnSecondaryDisplaysEnabled= */ true);
-        mMumdUms = new UserManagerService(mSpiedContext, mMockPms, mMockUserDataPreparer,
-                mPackagesLock, mRealContext.getDataDir(), mUsers, mUsersOnSecondaryDisplays);
-        assertWithMessage("UserManagerService.isUsersOnSecondaryDisplaysEnabled()")
-                .that(mMumdUms.isUsersOnSecondaryDisplaysEnabled())
-                .isTrue();
-        mMumdUmi = LocalServices.getService(UserManagerInternal.class);
-        assertWithMessage("LocalServices.getService(UserManagerInternal.class)").that(mMumdUmi)
+        // Must construct UserManagerService in the UiThread
+        mUms = new UserManagerService(mSpiedContext, mMockPms, mMockUserDataPreparer,
+                mPackagesLock, mRealContext.getDataDir(), mUsers);
+        mUmi = LocalServices.getService(UserManagerInternal.class);
+        assertWithMessage("LocalServices.getService(UserManagerInternal.class)").that(mUmi)
                 .isNotNull();
-        resetUserManagerInternal();
-
-        // mStandardUms / mStandardUmi
-        mockIsUsersOnSecondaryDisplaysEnabled(/* usersOnSecondaryDisplaysEnabled= */ false);
-        mStandardUms = new UserManagerService(mSpiedContext, mMockPms, mMockUserDataPreparer,
-                mPackagesLock, mRealContext.getDataDir(), mUsers, mUsersOnSecondaryDisplays);
-        assertWithMessage("UserManagerService.isUsersOnSecondaryDisplaysEnabled()")
-                .that(mStandardUms.isUsersOnSecondaryDisplaysEnabled())
-                .isFalse();
-        mStandardUmi = LocalServices.getService(UserManagerInternal.class);
-        assertWithMessage("LocalServices.getService(UserManagerInternal.class)").that(mStandardUmi)
-                .isNotNull();
-        setServiceFixtures(/*usersOnSecondaryDisplaysEnabled= */ false);
     }
 
     @After
@@ -185,373 +135,10 @@
         LocalServices.removeServiceForTest(UserManagerInternal.class);
     }
 
-    //////////////////////////////////////////////////////////////////////////////////////////////
-    // Methods whose UMS implementation calls UMI or vice-versa - they're tested in this class, //
-    // but the subclass must provide the proper implementation                                  //
-    //////////////////////////////////////////////////////////////////////////////////////////////
-
-    protected abstract boolean isUserVisible(int userId);
-    protected abstract boolean isUserVisibleOnDisplay(int userId, int displayId);
-    protected abstract int getDisplayAssignedToUser(int userId);
-    protected abstract int getUserAssignedToDisplay(int displayId);
-
-    /////////////////////////////////
-    // Tests for the above methods //
-    /////////////////////////////////
-
-    @Test
-    public void testIsUserVisible_invalidUser() {
-        mockCurrentUser(USER_ID);
-
-        assertWithMessage("isUserVisible(%s)", USER_NULL).that(isUserVisible(USER_NULL)).isFalse();
-    }
-
-    @Test
-    public void testIsUserVisible_currentUser() {
-        mockCurrentUser(USER_ID);
-
-        assertWithMessage("isUserVisible(%s)", USER_ID).that(isUserVisible(USER_ID)).isTrue();
-    }
-
-    @Test
-    public void testIsUserVisible_nonCurrentUser() {
-        mockCurrentUser(OTHER_USER_ID);
-
-        assertWithMessage("isUserVisible(%s)", USER_ID).that(isUserVisible(USER_ID)).isFalse();
-    }
-
-    @Test
-    public void testIsUserVisible_startedProfileOfcurrentUser() {
-        addDefaultProfileAndParent();
-        mockCurrentUser(PARENT_USER_ID);
-        startDefaultProfile();
-        setUserState(PROFILE_USER_ID, UserState.STATE_RUNNING_UNLOCKED);
-
-        assertWithMessage("isUserVisible(%s)", PROFILE_USER_ID).that(isUserVisible(PROFILE_USER_ID))
-                .isTrue();
-    }
-
-    @Test
-    public void testIsUserVisible_stoppedProfileOfcurrentUser() {
-        addDefaultProfileAndParent();
-        mockCurrentUser(PARENT_USER_ID);
-        stopDefaultProfile();
-
-        assertWithMessage("isUserVisible(%s)", PROFILE_USER_ID).that(isUserVisible(PROFILE_USER_ID))
-                .isFalse();
-    }
-
-    @Test
-    public void testIsUserVisible_bgUserOnSecondaryDisplay() {
-        enableUsersOnSecondaryDisplays();
-        mockCurrentUser(OTHER_USER_ID);
-        assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
-
-        assertWithMessage("isUserVisible(%s)", USER_ID).that(isUserVisible(USER_ID)).isTrue();
-    }
-
-    // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as
-    // isUserVisible() for bg users relies only on the user / display assignments
-
-    @Test
-    public void testIsUserVisibleOnDisplay_invalidUser() {
-        mockCurrentUser(USER_ID);
-
-        assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_NULL, DEFAULT_DISPLAY)
-                .that(isUserVisibleOnDisplay(USER_NULL, DEFAULT_DISPLAY)).isFalse();
-    }
-
-    @Test
-    public void testIsUserVisibleOnDisplay_currentUserInvalidDisplay() {
-        mockCurrentUser(USER_ID);
-
-        assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_ID, INVALID_DISPLAY)
-                .that(isUserVisibleOnDisplay(USER_ID, INVALID_DISPLAY)).isFalse();
-    }
-
-    @Test
-    public void testIsUserVisibleOnDisplay_currentUserDefaultDisplay() {
-        mockCurrentUser(USER_ID);
-
-        assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_ID, DEFAULT_DISPLAY)
-                .that(isUserVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY)).isTrue();
-    }
-
-    @Test
-    public void testIsUserVisibleOnDisplay_currentUserSecondaryDisplay() {
-        mockCurrentUser(USER_ID);
-
-        assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID)
-                .that(isUserVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID)).isTrue();
-    }
-
-    @Test
-    public void testIsUserVisibleOnDisplay_mumd_currentUserUnassignedSecondaryDisplay() {
-        enableUsersOnSecondaryDisplays();
-        mockCurrentUser(USER_ID);
-
-        assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID)
-                .that(isUserVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID)).isTrue();
-    }
-
-    @Test
-    public void testIsUserVisibleOnDisplay_mumd_currentUserSecondaryDisplayAssignedToAnotherUser() {
-        enableUsersOnSecondaryDisplays();
-        mockCurrentUser(USER_ID);
-        assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID);
-
-        assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID)
-                .that(isUserVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID)).isFalse();
-    }
-
-    @Test
-    public void testIsUserVisibleOnDisplay_mumd_startedProfileOfCurrentUserSecondaryDisplayAssignedToAnotherUser() {
-        enableUsersOnSecondaryDisplays();
-        addDefaultProfileAndParent();
-        startDefaultProfile();
-        mockCurrentUser(PARENT_USER_ID);
-        assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID);
-
-        assertWithMessage("isUserVisibleOnDisplay(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID)
-                .that(isUserVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isFalse();
-    }
-
-    @Test
-    public void testIsUserVisibleOnDisplay_mumd_stoppedProfileOfCurrentUserSecondaryDisplayAssignedToAnotherUser() {
-        enableUsersOnSecondaryDisplays();
-        addDefaultProfileAndParent();
-        stopDefaultProfile();
-        mockCurrentUser(PARENT_USER_ID);
-        assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID);
-
-        assertWithMessage("isUserVisibleOnDisplay(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID)
-                .that(isUserVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isFalse();
-    }
-
-    @Test
-    public void testIsUserVisibleOnDisplay_nonCurrentUserDefaultDisplay() {
-        mockCurrentUser(OTHER_USER_ID);
-
-        assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_ID, DEFAULT_DISPLAY)
-                .that(isUserVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY)).isFalse();
-    }
-
-    @Test
-    public void testIsUserVisibleOnDisplay_startedProfileOfcurrentUserInvalidDisplay() {
-        addDefaultProfileAndParent();
-        mockCurrentUser(PARENT_USER_ID);
-        startDefaultProfile();
-
-        assertWithMessage("isUserVisibleOnDisplay(%s, %s)", PROFILE_USER_ID, INVALID_DISPLAY)
-                .that(isUserVisibleOnDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY)).isTrue();
-    }
-
-    @Test
-    public void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserInvalidDisplay() {
-        addDefaultProfileAndParent();
-        mockCurrentUser(PARENT_USER_ID);
-        stopDefaultProfile();
-
-        assertWithMessage("isUserVisibleOnDisplay(%s, %s)", PROFILE_USER_ID, INVALID_DISPLAY)
-                .that(isUserVisibleOnDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY)).isFalse();
-    }
-
-    @Test
-    public void testIsUserVisibleOnDisplay_startedProfileOfcurrentUserDefaultDisplay() {
-        addDefaultProfileAndParent();
-        mockCurrentUser(PARENT_USER_ID);
-        startDefaultProfile();
-        setUserState(PROFILE_USER_ID, UserState.STATE_RUNNING_UNLOCKED);
-
-        assertWithMessage("isUserVisibleOnDisplay(%s, %s)", PROFILE_USER_ID, DEFAULT_DISPLAY)
-                .that(isUserVisibleOnDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY)).isTrue();
-    }
-
-    @Test
-    public void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserDefaultDisplay() {
-        addDefaultProfileAndParent();
-        mockCurrentUser(PARENT_USER_ID);
-        stopDefaultProfile();
-
-        assertWithMessage("isUserVisibleOnDisplay(%s, %s)", PROFILE_USER_ID, DEFAULT_DISPLAY)
-                .that(isUserVisibleOnDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY)).isFalse();
-    }
-
-    @Test
-    public void testIsUserVisibleOnDisplay_startedProfileOfcurrentUserSecondaryDisplay() {
-        addDefaultProfileAndParent();
-        mockCurrentUser(PARENT_USER_ID);
-        startDefaultProfile();
-        setUserState(PROFILE_USER_ID, UserState.STATE_RUNNING_UNLOCKED);
-
-        assertWithMessage("isUserVisibleOnDisplay(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID)
-                .that(isUserVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isTrue();
-    }
-
-    @Test
-    public void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserSecondaryDisplay() {
-        addDefaultProfileAndParent();
-        mockCurrentUser(PARENT_USER_ID);
-        stopDefaultProfile();
-
-        assertWithMessage("isUserVisibleOnDisplay(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID)
-                .that(isUserVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isFalse();
-    }
-
-    @Test
-    public void testIsUserVisibleOnDisplay_mumd_bgUserOnSecondaryDisplay() {
-        enableUsersOnSecondaryDisplays();
-        mockCurrentUser(OTHER_USER_ID);
-        assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
-
-        assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID)
-                .that(isUserVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID)).isTrue();
-    }
-
-    @Test
-    public void testIsUserVisibleOnDisplay_mumd_bgUserOnAnotherSecondaryDisplay() {
-        enableUsersOnSecondaryDisplays();
-        mockCurrentUser(OTHER_USER_ID);
-        assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
-
-        assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID)
-                .that(isUserVisibleOnDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID)).isFalse();
-    }
-
-    // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as
-    // isUserVisibleOnDisplay() for bg users relies only on the user / display assignments
-
-    @Test
-    public void testGetDisplayAssignedToUser_invalidUser() {
-        mockCurrentUser(USER_ID);
-
-        assertWithMessage("getDisplayAssignedToUser(%s)", USER_NULL)
-                .that(getDisplayAssignedToUser(USER_NULL)).isEqualTo(INVALID_DISPLAY);
-    }
-
-    @Test
-    public void testGetDisplayAssignedToUser_currentUser() {
-        mockCurrentUser(USER_ID);
-
-        assertWithMessage("getDisplayAssignedToUser(%s)", USER_ID)
-                .that(getDisplayAssignedToUser(USER_ID)).isEqualTo(DEFAULT_DISPLAY);
-    }
-
-    @Test
-    public void testGetDisplayAssignedToUser_nonCurrentUser() {
-        mockCurrentUser(OTHER_USER_ID);
-
-        assertWithMessage("getDisplayAssignedToUser(%s)", USER_ID)
-                .that(getDisplayAssignedToUser(USER_ID)).isEqualTo(INVALID_DISPLAY);
-    }
-
-    @Test
-    public void testGetDisplayAssignedToUser_startedProfileOfcurrentUser() {
-        addDefaultProfileAndParent();
-        mockCurrentUser(PARENT_USER_ID);
-        startDefaultProfile();
-        setUserState(PROFILE_USER_ID, UserState.STATE_RUNNING_UNLOCKED);
-
-        assertWithMessage("getDisplayAssignedToUser(%s)", PROFILE_USER_ID)
-                .that(getDisplayAssignedToUser(PROFILE_USER_ID)).isEqualTo(DEFAULT_DISPLAY);
-    }
-
-    @Test
-    public void testGetDisplayAssignedToUser_stoppedProfileOfcurrentUser() {
-        addDefaultProfileAndParent();
-        mockCurrentUser(PARENT_USER_ID);
-        stopDefaultProfile();
-
-        assertWithMessage("getDisplayAssignedToUser(%s)", PROFILE_USER_ID)
-                .that(getDisplayAssignedToUser(PROFILE_USER_ID)).isEqualTo(INVALID_DISPLAY);
-    }
-
-    @Test
-    public void testGetDisplayAssignedToUser_bgUserOnSecondaryDisplay() {
-        enableUsersOnSecondaryDisplays();
-        mockCurrentUser(OTHER_USER_ID);
-        assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
-
-        assertWithMessage("getDisplayAssignedToUser(%s)", USER_ID)
-                .that(getDisplayAssignedToUser(USER_ID)).isEqualTo(SECONDARY_DISPLAY_ID);
-    }
-
-    // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as
-    // getDisplayAssignedToUser() for bg users relies only on the user / display assignments
-
-    @Test
-    public void testGetUserAssignedToDisplay_invalidDisplay() {
-        mockCurrentUser(USER_ID);
-
-        assertWithMessage("getUserAssignedToDisplay(%s)", INVALID_DISPLAY)
-                .that(getUserAssignedToDisplay(INVALID_DISPLAY)).isEqualTo(USER_ID);
-    }
-
-    @Test
-    public void testGetUserAssignedToDisplay_defaultDisplay() {
-        mockCurrentUser(USER_ID);
-
-        assertWithMessage("getUserAssignedToDisplay(%s)", DEFAULT_DISPLAY)
-                .that(getUserAssignedToDisplay(DEFAULT_DISPLAY)).isEqualTo(USER_ID);
-    }
-
-    @Test
-    public void testGetUserAssignedToDisplay_secondaryDisplay() {
-        mockCurrentUser(USER_ID);
-
-        assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID)
-                .that(getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID);
-    }
-
-    @Test
-    public void testGetUserAssignedToDisplay_mumd_bgUserOnSecondaryDisplay() {
-        enableUsersOnSecondaryDisplays();
-        mockCurrentUser(OTHER_USER_ID);
-        assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
-
-        assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID)
-                .that(getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID);
-    }
-
-    @Test
-    public void testGetUserAssignedToDisplay_mumd_noUserOnSecondaryDisplay() {
-        enableUsersOnSecondaryDisplays();
-        mockCurrentUser(USER_ID);
-
-        assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID)
-                .that(getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID);
-    }
-
-    // TODO(b/244644281): scenario below shouldn't happen on "real life", as the profile cannot be
-    // started on secondary display if its parent isn't, so we might need to remove (or refactor
-    // this test) if/when the underlying logic changes
-    @Test
-    public void testGetUserAssignedToDisplay_mumd_profileOnSecondaryDisplay() {
-        enableUsersOnSecondaryDisplays();
-        addDefaultProfileAndParent();
-        mockCurrentUser(USER_ID);
-        assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
-
-        assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID)
-                .that(getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID);
-    }
-
-    // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as
-    // getUserAssignedToDisplay() for bg users relies only on the user / display assignments
-
-
     ///////////////////////////////////////////
     // Helper methods exposed to sub-classes //
     ///////////////////////////////////////////
 
-    /**
-     * Change test fixtures to use a version that supports {@code MUMD} (Multiple Users on Multiple
-     * Displays).
-     */
-    protected final void enableUsersOnSecondaryDisplays() {
-        setServiceFixtures(/* usersOnSecondaryDisplaysEnabled= */ true);
-    }
-
     protected final void mockCurrentUser(@UserIdInt int userId) {
         mockGetLocalService(ActivityManagerInternal.class, mActivityManagerInternal);
 
@@ -597,65 +184,19 @@
         setUserState(userId, UserState.STATE_STOPPING);
     }
 
-    // NOTE: should only called by tests that indirectly needs to check user assignments (like
-    // isUserVisible), not by tests for the user assignment methods per se.
-    protected final void assignUserToDisplay(@UserIdInt int userId, int displayId) {
-        mUsersOnSecondaryDisplays.put(userId, displayId);
-    }
-
-    protected final void assertNoUserAssignedToDisplay() {
-        assertWithMessage("mUsersOnSecondaryDisplays()").that(usersOnSecondaryDisplaysAsMap())
-                .isEmpty();
-    }
-
-    protected final void assertUserAssignedToDisplay(@UserIdInt int userId, int displayId) {
-        assertWithMessage("mUsersOnSecondaryDisplays()").that(usersOnSecondaryDisplaysAsMap())
-                .containsExactly(userId, displayId);
+    protected final void setUserState(@UserIdInt int userId, int userState) {
+        mUmi.setUserState(userId, userState);
     }
 
     ///////////////////
     // Private infra //
     ///////////////////
 
-    private void setServiceFixtures(boolean usersOnSecondaryDisplaysEnabled) {
-        Log.d(TAG, "Setting fixtures for usersOnSecondaryDisplaysEnabled="
-                + usersOnSecondaryDisplaysEnabled);
-        if (usersOnSecondaryDisplaysEnabled) {
-            mUms = mMumdUms;
-            mUmi = mMumdUmi;
-        } else {
-            mUms = mStandardUms;
-            mUmi = mStandardUmi;
-        }
-    }
-
-    private void mockIsUsersOnSecondaryDisplaysEnabled(boolean enabled) {
-        Log.d(TAG, "Mocking UserManager.isUsersOnSecondaryDisplaysEnabled() to return " + enabled);
-        doReturn(enabled).when(() -> UserManager.isUsersOnSecondaryDisplaysEnabled());
-    }
-
     private void addUserData(TestUserData userData) {
         Log.d(TAG, "Adding " + userData);
         mUsers.put(userData.info.id, userData);
     }
 
-    private void setUserState(@UserIdInt int userId, int userState) {
-        mUmi.setUserState(userId, userState);
-    }
-
-    private void removeUserState(@UserIdInt int userId) {
-        mUmi.removeUserState(userId);
-    }
-
-    private Map<Integer, Integer> usersOnSecondaryDisplaysAsMap() {
-        int size = mUsersOnSecondaryDisplays.size();
-        Map<Integer, Integer> map = new LinkedHashMap<>(size);
-        for (int i = 0; i < size; i++) {
-            map.put(mUsersOnSecondaryDisplays.keyAt(i), mUsersOnSecondaryDisplays.valueAt(i));
-        }
-        return map;
-    }
-
     private static final class TestUserData extends UserData {
 
         @SuppressWarnings("deprecation")
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 8b5921c..b5ffe5f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -123,24 +123,4 @@
         assertWithMessage("isUserRunning(%s)", PROFILE_USER_ID)
                 .that(mUms.isUserRunning(PROFILE_USER_ID)).isFalse();
     }
-
-    @Override
-    protected boolean isUserVisible(int userId) {
-        return mUms.isUserVisibleUnchecked(userId);
-    }
-
-    @Override
-    protected boolean isUserVisibleOnDisplay(int userId, int displayId) {
-        return mUms.isUserVisibleOnDisplay(userId, displayId);
-    }
-
-    @Override
-    protected int getDisplayAssignedToUser(int userId) {
-        return mUms.getDisplayAssignedToUser(userId);
-    }
-
-    @Override
-    protected int getUserAssignedToDisplay(int displayId) {
-        return mUms.getUserAssignedToDisplay(displayId);
-    }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java
new file mode 100644
index 0000000..21f541f
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java
@@ -0,0 +1,303 @@
+/*
+ * 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.pm;
+
+import static android.os.UserHandle.USER_SYSTEM;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+
+import android.util.Log;
+
+import org.junit.Test;
+
+/**
+ * Tests for {@link UserVisibilityMediator} tests for devices that support concurrent Multiple
+ * Users on Multiple Displays (A.K.A {@code MUMD}).
+ *
+ * <p>Run as
+ * {@code atest FrameworksMockingServicesTests:com.android.server.pm.UserVisibilityMediatorMUMDTest}
+ */
+public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediatorTestCase {
+
+    private static final String TAG = UserVisibilityMediatorMUMDTest.class.getSimpleName();
+
+    public UserVisibilityMediatorMUMDTest() {
+        super(/* usersOnSecondaryDisplaysEnabled= */ true);
+    }
+
+    @Test
+    public void testAssignUserToDisplay_systemUser() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mMediator.assignUserToDisplay(USER_SYSTEM, SECONDARY_DISPLAY_ID));
+    }
+
+    @Test
+    public void testAssignUserToDisplay_invalidDisplay() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mMediator.assignUserToDisplay(USER_ID, INVALID_DISPLAY));
+    }
+
+    @Test
+    public void testAssignUserToDisplay_currentUser() {
+        mockCurrentUser(USER_ID);
+
+        assertThrows(IllegalArgumentException.class,
+                () -> mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID));
+
+        assertNoUserAssignedToDisplay();
+    }
+
+    @Test
+    public void testAssignUserToDisplay_startedProfileOfCurrentUser() {
+        mockCurrentUser(PARENT_USER_ID);
+        addDefaultProfileAndParent();
+        startDefaultProfile();
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
+
+        Log.v(TAG, "Exception: " + e);
+        assertNoUserAssignedToDisplay();
+    }
+
+    @Test
+    public void testAssignUserToDisplay_stoppedProfileOfCurrentUser() {
+        mockCurrentUser(PARENT_USER_ID);
+        addDefaultProfileAndParent();
+        stopDefaultProfile();
+
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
+
+        Log.v(TAG, "Exception: " + e);
+        assertNoUserAssignedToDisplay();
+    }
+
+    @Test
+    public void testAssignUserToDisplay_displayAvailable() {
+        mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+
+        assertUserAssignedToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+    }
+
+    @Test
+    public void testAssignUserToDisplay_displayAlreadyAssigned() {
+        mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+
+        IllegalStateException e = assertThrows(IllegalStateException.class,
+                () -> mMediator.assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID));
+
+        Log.v(TAG, "Exception: " + e);
+        assertWithMessage("exception (%s) message", e).that(e).hasMessageThat()
+                .matches("Cannot.*" + OTHER_USER_ID + ".*" + SECONDARY_DISPLAY_ID + ".*already.*"
+                        + USER_ID + ".*");
+    }
+
+    @Test
+    public void testAssignUserToDisplay_userAlreadyAssigned() {
+        mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+
+        IllegalStateException e = assertThrows(IllegalStateException.class,
+                () -> mMediator.assignUserToDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID));
+
+        Log.v(TAG, "Exception: " + e);
+        assertWithMessage("exception (%s) message", e).that(e).hasMessageThat()
+                .matches("Cannot.*" + USER_ID + ".*" + OTHER_SECONDARY_DISPLAY_ID + ".*already.*"
+                        + SECONDARY_DISPLAY_ID + ".*");
+
+        assertUserAssignedToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+    }
+
+    @Test
+    public void testAssignUserToDisplay_profileOnSameDisplayAsParent() {
+        addDefaultProfileAndParent();
+
+        mMediator.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
+
+        Log.v(TAG, "Exception: " + e);
+        assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+    }
+
+    @Test
+    public void testAssignUserToDisplay_profileOnDifferentDisplayAsParent() {
+        addDefaultProfileAndParent();
+
+        mMediator.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, OTHER_SECONDARY_DISPLAY_ID));
+
+        Log.v(TAG, "Exception: " + e);
+        assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+    }
+
+    @Test
+    public void testAssignUserToDisplay_profileDefaultDisplayParentOnSecondaryDisplay() {
+        addDefaultProfileAndParent();
+
+        mMediator.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY));
+
+        Log.v(TAG, "Exception: " + e);
+        assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+    }
+
+    @Test
+    public void testUnassignUserFromDisplay() {
+        testAssignUserToDisplay_displayAvailable();
+
+        mMediator.unassignUserFromDisplay(USER_ID);
+
+        assertNoUserAssignedToDisplay();
+    }
+
+    @Test
+    public void testIsUserVisible_bgUserOnSecondaryDisplay() {
+        mockCurrentUser(OTHER_USER_ID);
+        assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+
+        assertWithMessage("isUserVisible(%s)", USER_ID)
+                .that(mMediator.isUserVisible(USER_ID)).isTrue();
+    }
+
+    // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as
+    // isUserVisible() for bg users relies only on the user / display assignments
+
+    @Test
+    public void testIsUserVisibleOnDisplay_currentUserUnassignedSecondaryDisplay() {
+        mockCurrentUser(USER_ID);
+
+        assertWithMessage("isUserVisible(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID)
+                .that(mMediator.isUserVisible(USER_ID, SECONDARY_DISPLAY_ID)).isTrue();
+    }
+
+    @Test
+    public void testIsUserVisibleOnDisplay_currentUserSecondaryDisplayAssignedToAnotherUser() {
+        mockCurrentUser(USER_ID);
+        assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID);
+
+        assertWithMessage("isUserVisible(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID)
+                .that(mMediator.isUserVisible(USER_ID, SECONDARY_DISPLAY_ID)).isFalse();
+    }
+
+    @Test
+    public void testIsUserVisibleOnDisplay_startedProfileOfCurrentUserSecondaryDisplayAssignedToAnotherUser() {
+        addDefaultProfileAndParent();
+        startDefaultProfile();
+        mockCurrentUser(PARENT_USER_ID);
+        assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID);
+
+        assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID)
+                .that(mMediator.isUserVisible(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isFalse();
+    }
+
+    @Test
+    public void testIsUserVisibleOnDisplay_stoppedProfileOfCurrentUserSecondaryDisplayAssignedToAnotherUser() {
+        addDefaultProfileAndParent();
+        stopDefaultProfile();
+        mockCurrentUser(PARENT_USER_ID);
+        assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID);
+
+        assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID)
+                .that(mMediator.isUserVisible(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isFalse();
+    }
+
+    @Test
+    public void testIsUserVisibleOnDisplay_startedProfileOfCurrentUserOnUnassignedSecondaryDisplay() {
+        addDefaultProfileAndParent();
+        startDefaultProfile();
+        mockCurrentUser(PARENT_USER_ID);
+
+        // TODO(b/244644281): change it to isFalse() once isUserVisible() is fixed (see note there)
+        assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID)
+                .that(mMediator.isUserVisible(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isTrue();
+    }
+
+    @Test
+    public void testIsUserVisibleOnDisplay_bgUserOnSecondaryDisplay() {
+        mockCurrentUser(OTHER_USER_ID);
+        assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+
+        assertWithMessage("isUserVisible(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID)
+                .that(mMediator.isUserVisible(USER_ID, SECONDARY_DISPLAY_ID)).isTrue();
+    }
+
+    @Test
+    public void testIsUserVisibleOnDisplay_bgUserOnAnotherSecondaryDisplay() {
+        mockCurrentUser(OTHER_USER_ID);
+        assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+
+        assertWithMessage("isUserVisible(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID)
+                .that(mMediator.isUserVisible(USER_ID, OTHER_SECONDARY_DISPLAY_ID)).isFalse();
+    }
+
+    // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as
+    // the tests for isUserVisible(userId, display) for non-current users relies on the explicit
+    // user / display assignments
+    // TODO(b/244644281): add such tests if the logic change
+
+    @Test
+    public void testGetDisplayAssignedToUser_bgUserOnSecondaryDisplay() {
+        mockCurrentUser(OTHER_USER_ID);
+        assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+
+        assertWithMessage("getDisplayAssignedToUser(%s)", USER_ID)
+                .that(mMediator.getDisplayAssignedToUser(USER_ID))
+                .isEqualTo(SECONDARY_DISPLAY_ID);
+    }
+
+    // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as
+    // getDisplayAssignedToUser() for bg users relies only on the user / display assignments
+
+    @Test
+    public void testGetUserAssignedToDisplay_bgUserOnSecondaryDisplay() {
+        mockCurrentUser(OTHER_USER_ID);
+        assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+
+        assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID)
+                .that(mMediator.getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID);
+    }
+
+    @Test
+    public void testGetUserAssignedToDisplay_noUserOnSecondaryDisplay() {
+        mockCurrentUser(USER_ID);
+
+        assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID)
+                .that(mMediator.getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID);
+    }
+
+    // TODO(b/244644281): scenario below shouldn't happen on "real life", as the profile cannot be
+    // started on secondary display if its parent isn't, so we might need to remove (or refactor
+    // this test) if/when the underlying logic changes
+    @Test
+    public void testGetUserAssignedToDisplay_profileOnSecondaryDisplay() {
+        addDefaultProfileAndParent();
+        mockCurrentUser(USER_ID);
+        assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
+
+        assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID)
+                .that(mMediator.getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID);
+    }
+
+    // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as
+    // getUserAssignedToDisplay() for bg users relies only on the user / display assignments
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java
new file mode 100644
index 0000000..7ae8117
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.pm;
+
+import static android.os.UserHandle.USER_SYSTEM;
+
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+
+/**
+ * Tests for {@link UserVisibilityMediator} tests for devices that DO NOT support concurrent
+ * multiple users on multiple displays (A.K.A {@code SUSD} - Single User on Single Device).
+ *
+ * <p>Run as
+ * {@code atest FrameworksMockingServicesTests:com.android.server.pm.UserVisibilityMediatorSUSDTest}
+ */
+public final class UserVisibilityMediatorSUSDTest extends UserVisibilityMediatorTestCase {
+
+    public UserVisibilityMediatorSUSDTest() {
+        super(/* usersOnSecondaryDisplaysEnabled= */ false);
+    }
+
+    @Test
+    public void testAssignUserToDisplay_otherDisplay_currentUser() {
+        mockCurrentUser(USER_ID);
+
+        assertThrows(UnsupportedOperationException.class,
+                () -> mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID));
+    }
+
+    @Test
+    public void testAssignUserToDisplay_otherDisplay_startProfileOfcurrentUser() {
+        mockCurrentUser(PARENT_USER_ID);
+        addDefaultProfileAndParent();
+        startDefaultProfile();
+
+        assertThrows(UnsupportedOperationException.class,
+                () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
+    }
+
+    @Test
+    public void testAssignUserToDisplay_otherDisplay_stoppedProfileOfcurrentUser() {
+        mockCurrentUser(PARENT_USER_ID);
+        addDefaultProfileAndParent();
+        stopDefaultProfile();
+
+        assertThrows(UnsupportedOperationException.class,
+                () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
+    }
+
+    @Test
+    public void testUnassignUserFromDisplay_ignored() {
+        mockCurrentUser(USER_ID);
+
+        mMediator.unassignUserFromDisplay(USER_SYSTEM);
+        mMediator.unassignUserFromDisplay(USER_ID);
+        mMediator.unassignUserFromDisplay(OTHER_USER_ID);
+
+        assertNoUserAssignedToDisplay();
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
new file mode 100644
index 0000000..22e6e0d
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
@@ -0,0 +1,323 @@
+/*
+ * 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.pm;
+
+import static android.os.UserHandle.USER_NULL;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
+
+import static com.android.server.am.UserState.STATE_RUNNING_UNLOCKED;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.annotation.UserIdInt;
+import android.util.SparseIntArray;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Base class for {@link UserVisibilityMediator} tests.
+ *
+ * <p>It contains common logics and tests for behaviors that should be invariant regardless of the
+ * device mode (for example, whether the device supports concurrent multiple users on multiple
+ * displays or not).
+ */
+abstract class UserVisibilityMediatorTestCase extends UserManagerServiceOrInternalTestCase {
+
+    /**
+     * Id of a secondary display (i.e, not {@link android.view.Display.DEFAULT_DISPLAY}).
+     */
+    protected static final int SECONDARY_DISPLAY_ID = 42;
+
+    /**
+     * Id of another secondary display (i.e, not {@link android.view.Display.DEFAULT_DISPLAY}).
+     */
+    protected static final int OTHER_SECONDARY_DISPLAY_ID = 108;
+
+    private final boolean mUsersOnSecondaryDisplaysEnabled;
+
+    // TODO(b/244644281): manipulating mUsersOnSecondaryDisplays directly leaks implementation
+    // details into the unit test, but it's fine for now as the tests were copied "as is" - it
+    // would be better to use a geter() instead
+    protected final SparseIntArray mUsersOnSecondaryDisplays = new SparseIntArray();
+
+    protected UserVisibilityMediator mMediator;
+
+    protected UserVisibilityMediatorTestCase(boolean usersOnSecondaryDisplaysEnabled) {
+        mUsersOnSecondaryDisplaysEnabled = usersOnSecondaryDisplaysEnabled;
+    }
+
+    @Before
+    public final void setMediator() {
+        mMediator = new UserVisibilityMediator(mUms, mUsersOnSecondaryDisplaysEnabled,
+                mUsersOnSecondaryDisplays);
+    }
+
+    @Test
+    public final void testAssignUserToDisplay_defaultDisplayIgnored() {
+        mMediator.assignUserToDisplay(USER_ID, DEFAULT_DISPLAY);
+
+        assertNoUserAssignedToDisplay();
+    }
+
+    @Test
+    public final void testIsUserVisible_invalidUser() {
+        mockCurrentUser(USER_ID);
+
+        assertWithMessage("isUserVisible(%s)", USER_NULL)
+                .that(mMediator.isUserVisible(USER_NULL)).isFalse();
+    }
+
+    @Test
+    public final void testIsUserVisible_currentUser() {
+        mockCurrentUser(USER_ID);
+
+        assertWithMessage("isUserVisible(%s)", USER_ID)
+                .that(mMediator.isUserVisible(USER_ID)).isTrue();
+    }
+
+    @Test
+    public final void testIsUserVisible_nonCurrentUser() {
+        mockCurrentUser(OTHER_USER_ID);
+
+        assertWithMessage("isUserVisible(%s)", USER_ID)
+                .that(mMediator.isUserVisible(USER_ID)).isFalse();
+    }
+
+    @Test
+    public final void testIsUserVisible_startedProfileOfcurrentUser() {
+        addDefaultProfileAndParent();
+        mockCurrentUser(PARENT_USER_ID);
+        startDefaultProfile();
+        setUserState(PROFILE_USER_ID, STATE_RUNNING_UNLOCKED);
+
+        assertWithMessage("isUserVisible(%s)", PROFILE_USER_ID)
+                .that(mMediator.isUserVisible(PROFILE_USER_ID)).isTrue();
+    }
+
+    @Test
+    public final void testIsUserVisible_stoppedProfileOfcurrentUser() {
+        addDefaultProfileAndParent();
+        mockCurrentUser(PARENT_USER_ID);
+        stopDefaultProfile();
+
+        assertWithMessage("isUserVisible(%s)", PROFILE_USER_ID)
+                .that(mMediator.isUserVisible(PROFILE_USER_ID)).isFalse();
+    }
+
+    @Test
+    public final void testIsUserVisibleOnDisplay_invalidUser() {
+        mockCurrentUser(USER_ID);
+
+        assertWithMessage("isUserVisible(%s, %s)", USER_NULL, DEFAULT_DISPLAY)
+                .that(mMediator.isUserVisible(USER_NULL, DEFAULT_DISPLAY)).isFalse();
+    }
+
+    @Test
+    public final void testIsUserVisibleOnDisplay_currentUserInvalidDisplay() {
+        mockCurrentUser(USER_ID);
+
+        assertWithMessage("isUserVisible(%s, %s)", USER_ID, INVALID_DISPLAY)
+                .that(mMediator.isUserVisible(USER_ID, INVALID_DISPLAY)).isFalse();
+    }
+
+    @Test
+    public final void testIsUserVisibleOnDisplay_currentUserDefaultDisplay() {
+        mockCurrentUser(USER_ID);
+
+        assertWithMessage("isUserVisible(%s, %s)", USER_ID, DEFAULT_DISPLAY)
+                .that(mMediator.isUserVisible(USER_ID, DEFAULT_DISPLAY)).isTrue();
+    }
+
+    @Test
+    public final void testIsUserVisibleOnDisplay_currentUserSecondaryDisplay() {
+        mockCurrentUser(USER_ID);
+
+        assertWithMessage("isUserVisible(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID)
+                .that(mMediator.isUserVisible(USER_ID, SECONDARY_DISPLAY_ID)).isTrue();
+    }
+
+    @Test
+    public final void testIsUserVisibleOnDisplay_nonCurrentUserDefaultDisplay() {
+        mockCurrentUser(OTHER_USER_ID);
+
+        assertWithMessage("isUserVisible(%s, %s)", USER_ID, DEFAULT_DISPLAY)
+                .that(mMediator.isUserVisible(USER_ID, DEFAULT_DISPLAY)).isFalse();
+    }
+
+    @Test
+    public final void testIsUserVisibleOnDisplay_startedProfileOfcurrentUserInvalidDisplay() {
+        addDefaultProfileAndParent();
+        mockCurrentUser(PARENT_USER_ID);
+        startDefaultProfile();
+
+        assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, INVALID_DISPLAY)
+                .that(mMediator.isUserVisible(PROFILE_USER_ID, DEFAULT_DISPLAY)).isTrue();
+    }
+
+    @Test
+    public final void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserInvalidDisplay() {
+        addDefaultProfileAndParent();
+        mockCurrentUser(PARENT_USER_ID);
+        stopDefaultProfile();
+
+        assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, INVALID_DISPLAY)
+                .that(mMediator.isUserVisible(PROFILE_USER_ID, DEFAULT_DISPLAY)).isFalse();
+    }
+
+    @Test
+    public final void testIsUserVisibleOnDisplay_startedProfileOfcurrentUserDefaultDisplay() {
+        addDefaultProfileAndParent();
+        mockCurrentUser(PARENT_USER_ID);
+        startDefaultProfile();
+        setUserState(PROFILE_USER_ID, STATE_RUNNING_UNLOCKED);
+
+        assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, DEFAULT_DISPLAY)
+                .that(mMediator.isUserVisible(PROFILE_USER_ID, DEFAULT_DISPLAY)).isTrue();
+    }
+
+    @Test
+    public final void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserDefaultDisplay() {
+        addDefaultProfileAndParent();
+        mockCurrentUser(PARENT_USER_ID);
+        stopDefaultProfile();
+
+        assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, DEFAULT_DISPLAY)
+                .that(mMediator.isUserVisible(PROFILE_USER_ID, DEFAULT_DISPLAY)).isFalse();
+    }
+
+    @Test
+    public final void testIsUserVisibleOnDisplay_startedProfileOfCurrentUserSecondaryDisplay() {
+        addDefaultProfileAndParent();
+        mockCurrentUser(PARENT_USER_ID);
+        startDefaultProfile();
+        setUserState(PROFILE_USER_ID, STATE_RUNNING_UNLOCKED);
+
+        assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID)
+                .that(mMediator.isUserVisible(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isTrue();
+    }
+
+    @Test
+    public void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserSecondaryDisplay() {
+        addDefaultProfileAndParent();
+        mockCurrentUser(PARENT_USER_ID);
+        stopDefaultProfile();
+
+        assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID)
+                .that(mMediator.isUserVisible(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isFalse();
+    }
+
+    @Test
+    public void testGetDisplayAssignedToUser_invalidUser() {
+        mockCurrentUser(USER_ID);
+
+        assertWithMessage("getDisplayAssignedToUser(%s)", USER_NULL)
+                .that(mMediator.getDisplayAssignedToUser(USER_NULL)).isEqualTo(INVALID_DISPLAY);
+    }
+
+    @Test
+    public void testGetDisplayAssignedToUser_currentUser() {
+        mockCurrentUser(USER_ID);
+
+        assertWithMessage("getDisplayAssignedToUser(%s)", USER_ID)
+                .that(mMediator.getDisplayAssignedToUser(USER_ID)).isEqualTo(DEFAULT_DISPLAY);
+    }
+
+    @Test
+    public final void testGetDisplayAssignedToUser_nonCurrentUser() {
+        mockCurrentUser(OTHER_USER_ID);
+
+        assertWithMessage("getDisplayAssignedToUser(%s)", USER_ID)
+                .that(mMediator.getDisplayAssignedToUser(USER_ID)).isEqualTo(INVALID_DISPLAY);
+    }
+
+    @Test
+    public final void testGetDisplayAssignedToUser_startedProfileOfcurrentUser() {
+        addDefaultProfileAndParent();
+        mockCurrentUser(PARENT_USER_ID);
+        startDefaultProfile();
+        setUserState(PROFILE_USER_ID, STATE_RUNNING_UNLOCKED);
+
+        assertWithMessage("getDisplayAssignedToUser(%s)", PROFILE_USER_ID)
+                .that(mMediator.getDisplayAssignedToUser(PROFILE_USER_ID))
+                .isEqualTo(DEFAULT_DISPLAY);
+    }
+
+    @Test
+    public final void testGetDisplayAssignedToUser_stoppedProfileOfcurrentUser() {
+        addDefaultProfileAndParent();
+        mockCurrentUser(PARENT_USER_ID);
+        stopDefaultProfile();
+
+        assertWithMessage("getDisplayAssignedToUser(%s)", PROFILE_USER_ID)
+                .that(mMediator.getDisplayAssignedToUser(PROFILE_USER_ID))
+                .isEqualTo(INVALID_DISPLAY);
+    }
+
+    @Test
+    public void testGetUserAssignedToDisplay_invalidDisplay() {
+        mockCurrentUser(USER_ID);
+
+        assertWithMessage("getUserAssignedToDisplay(%s)", INVALID_DISPLAY)
+                .that(mMediator.getUserAssignedToDisplay(INVALID_DISPLAY)).isEqualTo(USER_ID);
+    }
+
+    @Test
+    public final void testGetUserAssignedToDisplay_defaultDisplay() {
+        mockCurrentUser(USER_ID);
+
+        assertWithMessage("getUserAssignedToDisplay(%s)", DEFAULT_DISPLAY)
+                .that(mMediator.getUserAssignedToDisplay(DEFAULT_DISPLAY)).isEqualTo(USER_ID);
+    }
+
+    @Test
+    public final void testGetUserAssignedToDisplay_secondaryDisplay() {
+        mockCurrentUser(USER_ID);
+
+        assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID)
+                .that(mMediator.getUserAssignedToDisplay(SECONDARY_DISPLAY_ID))
+                .isEqualTo(USER_ID);
+    }
+
+    // NOTE: should only called by tests that indirectly needs to check user assignments (like
+    // isUserVisible), not by tests for the user assignment methods per se.
+    protected final void assignUserToDisplay(@UserIdInt int userId, int displayId) {
+        mUsersOnSecondaryDisplays.put(userId, displayId);
+    }
+
+    protected final void assertNoUserAssignedToDisplay() {
+        assertWithMessage("mUsersOnSecondaryDisplays()").that(usersOnSecondaryDisplaysAsMap())
+                .isEmpty();
+    }
+
+    protected final void assertUserAssignedToDisplay(@UserIdInt int userId, int displayId) {
+        assertWithMessage("mUsersOnSecondaryDisplays()").that(usersOnSecondaryDisplaysAsMap())
+                .containsExactly(userId, displayId);
+    }
+
+    private Map<Integer, Integer> usersOnSecondaryDisplaysAsMap() {
+        int size = mUsersOnSecondaryDisplays.size();
+        Map<Integer, Integer> map = new LinkedHashMap<>(size);
+        for (int i = 0; i < size; i++) {
+            map.put(mUsersOnSecondaryDisplays.keyAt(i), mUsersOnSecondaryDisplays.valueAt(i));
+        }
+        return map;
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java
index 5012335..21c9c75 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java
@@ -61,7 +61,7 @@
 
     @Test
     public void noopWhenBothNull() {
-        final SensorOverlays useless = new SensorOverlays(null, null);
+        final SensorOverlays useless = new SensorOverlays(null, null, null);
         useless.show(SENSOR_ID, 2, null);
         useless.hide(SENSOR_ID);
     }
@@ -69,12 +69,12 @@
     @Test
     public void testProvidesUdfps() {
         final List<IUdfpsOverlayController> udfps = new ArrayList<>();
-        SensorOverlays sensorOverlays = new SensorOverlays(null, mSidefpsController);
+        SensorOverlays sensorOverlays = new SensorOverlays(null, mSidefpsController, null);
 
         sensorOverlays.ifUdfps(udfps::add);
         assertThat(udfps).isEmpty();
 
-        sensorOverlays = new SensorOverlays(mUdfpsOverlayController, mSidefpsController);
+        sensorOverlays = new SensorOverlays(mUdfpsOverlayController, mSidefpsController, null);
         sensorOverlays.ifUdfps(udfps::add);
         assertThat(udfps).containsExactly(mUdfpsOverlayController);
     }
@@ -96,7 +96,7 @@
 
     private void testShow(IUdfpsOverlayController udfps, ISidefpsController sidefps)
             throws Exception {
-        final SensorOverlays sensorOverlays = new SensorOverlays(udfps, sidefps);
+        final SensorOverlays sensorOverlays = new SensorOverlays(udfps, sidefps, null);
         final int reason = BiometricOverlayConstants.REASON_UNKNOWN;
         sensorOverlays.show(SENSOR_ID, reason, mAcquisitionClient);
 
@@ -126,7 +126,7 @@
 
     private void testHide(IUdfpsOverlayController udfps, ISidefpsController sidefps)
             throws Exception {
-        final SensorOverlays sensorOverlays = new SensorOverlays(udfps, sidefps);
+        final SensorOverlays sensorOverlays = new SensorOverlays(udfps, sidefps, null);
         sensorOverlays.hide(SENSOR_ID);
 
         if (udfps != null) {
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 1b5db0a..675f0e3 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
@@ -645,7 +645,7 @@
                 9 /* sensorId */, mBiometricLogger, mBiometricContext,
                 true /* isStrongBiometric */,
                 null /* taskStackListener */, mLockoutCache,
-                mUdfpsOverlayController, mSideFpsController, allowBackgroundAuthentication,
+                mUdfpsOverlayController, mSideFpsController, null, allowBackgroundAuthentication,
                 mSensorProps,
                 new Handler(mLooper.getLooper()), 0 /* biometricStrength */, mClock) {
             @Override
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClientTest.java
index 93cbef1..4579fc1 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClientTest.java
@@ -118,6 +118,6 @@
         return new FingerprintDetectClient(mContext, () -> aidl, mToken,
                 6 /* requestId */, mClientMonitorCallbackConverter, 2 /* userId */,
                 "a-test", 1 /* sensorId */, mBiometricLogger, mBiometricContext,
-                mUdfpsOverlayController, true /* isStrongBiometric */);
+                mUdfpsOverlayController, null, true /* isStrongBiometric */);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
index 837b553..38b06c4 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
@@ -28,7 +28,6 @@
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.same;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -66,7 +65,6 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
-import java.util.ArrayList;
 import java.util.function.Consumer;
 
 @Presubmit
@@ -292,6 +290,6 @@
         mClientMonitorCallbackConverter, 0 /* userId */,
         HAT, "owner", mBiometricUtils, 8 /* sensorId */,
         mBiometricLogger, mBiometricContext, mSensorProps, mUdfpsOverlayController,
-        mSideFpsController, 6 /* maxTemplatesPerUser */, FingerprintManager.ENROLL_ENROLL);
+        mSideFpsController, null, 6 /* maxTemplatesPerUser */, FingerprintManager.ENROLL_ENROLL);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 02bbe65..5fda3d6 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -891,4 +891,34 @@
         verify(mContext).startActivityAsUser(argThat(intent ->
                 intent.filterEquals(blockedAppIntent)), any(), any());
     }
+
+    @Test
+    public void registerRunningAppsChangedListener_onRunningAppsChanged_listenersNotified() {
+        ArraySet<Integer> uids = new ArraySet<>(Arrays.asList(UID_1, UID_2));
+        mDeviceImpl.onVirtualDisplayCreatedLocked(
+                mDeviceImpl.createWindowPolicyController(), DISPLAY_ID);
+        GenericWindowPolicyController gwpc = mDeviceImpl.getWindowPolicyControllersForTesting().get(
+                DISPLAY_ID);
+
+        gwpc.onRunningAppsChanged(uids);
+        mDeviceImpl.onRunningAppsChanged(uids);
+
+        assertThat(gwpc.getRunningAppsChangedListenersSizeForTesting()).isEqualTo(1);
+        verify(mRunningAppsChangedCallback).accept(new ArraySet<>(Arrays.asList(UID_1, UID_2)));
+    }
+
+    @Test
+    public void noRunningAppsChangedListener_onRunningAppsChanged_doesNotThrowException() {
+        ArraySet<Integer> uids = new ArraySet<>(Arrays.asList(UID_1, UID_2));
+        mDeviceImpl.onVirtualDisplayCreatedLocked(
+                mDeviceImpl.createWindowPolicyController(), DISPLAY_ID);
+        GenericWindowPolicyController gwpc = mDeviceImpl.getWindowPolicyControllersForTesting().get(
+                DISPLAY_ID);
+        mDeviceImpl.onVirtualDisplayRemovedLocked(DISPLAY_ID);
+
+        // This call should not throw any exceptions.
+        gwpc.onRunningAppsChanged(uids);
+
+        assertThat(gwpc.getRunningAppsChangedListenersSizeForTesting()).isEqualTo(0);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
index c68db34..6590a2b 100644
--- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
+++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
@@ -36,6 +36,7 @@
 import com.android.server.input.BatteryController.POLLING_PERIOD_MILLIS
 import com.android.server.input.BatteryController.UEventManager
 import com.android.server.input.BatteryController.UEventManager.UEventBatteryListener
+import com.android.server.input.BatteryController.USI_BATTERY_VALIDITY_DURATION_MILLIS
 import org.hamcrest.Description
 import org.hamcrest.Matcher
 import org.hamcrest.MatcherAssert.assertThat
@@ -528,10 +529,109 @@
             matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f))
 
         // The battery is no longer present after the timeout expires.
-        testLooper.moveTimeForward(BatteryController.USI_BATTERY_VALIDITY_DURATION_MILLIS - 1)
+        testLooper.moveTimeForward(USI_BATTERY_VALIDITY_DURATION_MILLIS - 1)
         testLooper.dispatchNext()
         listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID), times(2))
         assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
             isInvalidBatteryState(USI_DEVICE_ID))
     }
+
+    @Test
+    fun testStylusPresenceExtendsValidUsiBatteryState() {
+        `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device")
+        `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_DISCHARGING)
+        `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(78)
+
+        addInputDevice(USI_DEVICE_ID, supportsUsi = true)
+        testLooper.dispatchNext()
+        val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java)
+        verify(uEventManager)
+            .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device"))
+
+        // There is a UEvent signaling a battery change. The battery state is now valid.
+        uEventListener.value!!.onBatteryUEvent(TIMESTAMP)
+        val listener = createMockListener()
+        batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID)
+        listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)
+        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
+            matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f))
+
+        // Stylus presence is detected before the validity timeout expires.
+        testLooper.moveTimeForward(100)
+        testLooper.dispatchAll()
+        batteryController.notifyStylusGestureStarted(USI_DEVICE_ID, TIMESTAMP)
+
+        // Ensure that timeout was extended, and the battery state is now valid for longer.
+        testLooper.moveTimeForward(USI_BATTERY_VALIDITY_DURATION_MILLIS - 100)
+        testLooper.dispatchAll()
+        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
+            matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f))
+
+        // Ensure the validity period expires after the expected amount of time.
+        testLooper.moveTimeForward(100)
+        testLooper.dispatchNext()
+        listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID))
+        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
+            isInvalidBatteryState(USI_DEVICE_ID))
+    }
+
+    @Test
+    fun testStylusPresenceMakesUsiBatteryStateValid() {
+        `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device")
+        `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_DISCHARGING)
+        `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(78)
+
+        addInputDevice(USI_DEVICE_ID, supportsUsi = true)
+        testLooper.dispatchNext()
+        val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java)
+        verify(uEventManager)
+            .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device"))
+
+        // The USI battery state is initially invalid.
+        val listener = createMockListener()
+        batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID)
+        listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID))
+        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
+            isInvalidBatteryState(USI_DEVICE_ID))
+
+        // A stylus presence is detected. This validates the battery state.
+        batteryController.notifyStylusGestureStarted(USI_DEVICE_ID, TIMESTAMP)
+
+        listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)
+        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
+            matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f))
+    }
+
+    @Test
+    fun testStylusPresenceDoesNotMakeUsiBatteryStateValidAtBoot() {
+        `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device")
+        // At boot, the USI device always reports a capacity value of 0.
+        `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_UNKNOWN)
+        `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(0)
+
+        addInputDevice(USI_DEVICE_ID, supportsUsi = true)
+        testLooper.dispatchNext()
+        val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java)
+        verify(uEventManager)
+            .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device"))
+
+        // The USI battery state is initially invalid.
+        val listener = createMockListener()
+        batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID)
+        listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID))
+        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
+            isInvalidBatteryState(USI_DEVICE_ID))
+
+        // Since the capacity reported is 0, stylus presence does not validate the battery state.
+        batteryController.notifyStylusGestureStarted(USI_DEVICE_ID, TIMESTAMP)
+
+        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
+            isInvalidBatteryState(USI_DEVICE_ID))
+
+        // However, if a UEvent reports a battery capacity of 0, the battery state is now valid.
+        uEventListener.value!!.onBatteryUEvent(TIMESTAMP)
+        listener.verifyNotified(USI_DEVICE_ID, status = STATUS_UNKNOWN, capacity = 0f)
+        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
+            matchesState(USI_DEVICE_ID, status = STATUS_UNKNOWN, capacity = 0f))
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/job/WorkTypeConfigTest.java b/services/tests/servicestests/src/com/android/server/job/WorkTypeConfigTest.java
index a5fedef..21d2784 100644
--- a/services/tests/servicestests/src/com/android/server/job/WorkTypeConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/WorkTypeConfigTest.java
@@ -36,7 +36,6 @@
 
 import com.android.server.job.JobConcurrencyManager.WorkTypeConfig;
 
-import org.junit.After;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -59,30 +58,6 @@
     private static final String KEY_MIN_BGUSER_IMPORTANT = "concurrency_min_bguser_important_test";
     private static final String KEY_MIN_BGUSER = "concurrency_min_bguser_test";
 
-    @After
-    public void tearDown() throws Exception {
-        resetConfig();
-    }
-
-    private void resetConfig() {
-        // DeviceConfig.resetToDefaults() doesn't work here. Need to reset constants manually.
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MAX_TOTAL, null, false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MAX_TOP, null, false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MAX_FGS, null, false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MAX_EJ, null, false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MAX_BG, null, false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
-                KEY_MAX_BGUSER_IMPORTANT, null, false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MAX_BGUSER, null, false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MIN_TOP, null, false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MIN_FGS, null, false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MIN_EJ, null, false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MIN_BG, null, false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
-                KEY_MIN_BGUSER_IMPORTANT, null, false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MIN_BGUSER, null, false);
-    }
-
     private void check(@Nullable DeviceConfig.Properties config,
             int defaultTotal,
             @NonNull List<Pair<Integer, Integer>> defaultMin,
@@ -90,10 +65,6 @@
             boolean expectedValid, int expectedTotal,
             @NonNull List<Pair<Integer, Integer>> expectedMinLimits,
             @NonNull List<Pair<Integer, Integer>> expectedMaxLimits) throws Exception {
-        resetConfig();
-        if (config != null) {
-            DeviceConfig.setProperties(config);
-        }
 
         final WorkTypeConfig counts;
         try {
@@ -112,7 +83,9 @@
             }
         }
 
-        counts.update(DeviceConfig.getProperties(DeviceConfig.NAMESPACE_JOB_SCHEDULER));
+        if (config != null) {
+            counts.update(config);
+        }
 
         assertEquals(expectedTotal, counts.getMaxTotal());
         for (Pair<Integer, Integer> min : expectedMinLimits) {
diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java
index dbcd38c..dc9f907 100644
--- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java
@@ -229,6 +229,29 @@
         }
     }
 
+    @Test(expected = SecurityException.class)
+    public void testGetApplicationLocales_currentImeQueryNonForegroundAppLocales_fails()
+            throws Exception {
+        doReturn(DEFAULT_UID).when(mMockPackageManager)
+                .getPackageUidAsUser(anyString(), any(), anyInt());
+        doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES))
+                .when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt());
+        String imPkgName = getCurrentInputMethodPackageName();
+        doReturn(Binder.getCallingUid()).when(mMockPackageManager)
+                .getPackageUidAsUser(eq(imPkgName), any(), anyInt());
+        doReturn(false).when(mMockActivityManager).isAppForeground(anyInt());
+        setUpFailingPermissionCheckFor(Manifest.permission.READ_APP_SPECIFIC_LOCALES);
+
+        try {
+            mLocaleManagerService.getApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+            fail("Expected SecurityException");
+        } finally {
+            verify(mMockContext).enforceCallingOrSelfPermission(
+                    eq(android.Manifest.permission.READ_APP_SPECIFIC_LOCALES),
+                    anyString());
+        }
+    }
+
     @Test
     public void testGetApplicationLocales_appSpecificConfigAbsent_returnsEmptyList()
             throws Exception {
@@ -307,7 +330,7 @@
     }
 
     @Test
-    public void testGetApplicationLocales_callerIsCurrentInputMethod_returnsLocales()
+    public void testGetApplicationLocales_currentImeQueryForegroundAppLocales_returnsLocales()
             throws Exception {
         doReturn(DEFAULT_UID).when(mMockPackageManager)
                 .getPackageUidAsUser(anyString(), any(), anyInt());
@@ -316,6 +339,7 @@
         String imPkgName = getCurrentInputMethodPackageName();
         doReturn(Binder.getCallingUid()).when(mMockPackageManager)
                 .getPackageUidAsUser(eq(imPkgName), any(), anyInt());
+        doReturn(true).when(mMockActivityManager).isAppForeground(anyInt());
 
         LocaleList locales =
                 mLocaleManagerService.getApplicationLocales(
diff --git a/services/tests/servicestests/src/com/android/server/utils/EventLoggerTest.java b/services/tests/servicestests/src/com/android/server/utils/EventLoggerTest.java
index 0b27f87..4762696 100644
--- a/services/tests/servicestests/src/com/android/server/utils/EventLoggerTest.java
+++ b/services/tests/servicestests/src/com/android/server/utils/EventLoggerTest.java
@@ -125,7 +125,7 @@
         @Test
         public void testThatLoggingWorksAsExpected() {
             for (EventLogger.Event event: mEventsToInsert) {
-                mEventLogger.log(event);
+                mEventLogger.enqueue(event);
             }
 
             mEventLogger.dump(mTestPrintWriter);
diff --git a/services/tests/uiservicestests/src/com/android/server/slice/SliceManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/slice/SliceManagerServiceTest.java
index a917c57..6ef5b0e 100644
--- a/services/tests/uiservicestests/src/com/android/server/slice/SliceManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/slice/SliceManagerServiceTest.java
@@ -51,6 +51,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -87,6 +88,7 @@
         LocalServices.removeServiceForTest(UsageStatsManagerInternal.class);
     }
 
+    @Ignore("b/253871109")
     @Test
     public void testAddPinCreatesPinned() throws RemoteException {
         grantSlicePermission();
@@ -97,6 +99,7 @@
         verify(mService, times(1)).createPinnedSlice(eq(maybeAddUserId(TEST_URI, 0)), anyString());
     }
 
+    @Ignore("b/253871109")
     @Test
     public void testRemovePinDestroysPinned() throws RemoteException {
         grantSlicePermission();
@@ -109,6 +112,7 @@
         verify(mCreatedSliceState, never()).destroy();
     }
 
+    @Ignore("b/253871109")
     @Test
     public void testCheckAutoGrantPermissions() throws RemoteException {
         String[] testPerms = new String[] {
@@ -128,12 +132,14 @@
         verify(mContextSpy).checkPermission(eq("perm2"), eq(Process.myPid()), eq(Process.myUid()));
     }
 
+    @Ignore("b/253871109")
     @Test(expected = IllegalStateException.class)
     public void testNoPinThrow() throws Exception {
         grantSlicePermission();
         mService.getPinnedSpecs(TEST_URI, "pkg");
     }
 
+    @Ignore("b/253871109")
     @Test
     public void testGetPinnedSpecs() throws Exception {
         grantSlicePermission();
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 439eaa6..ef693b5 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -3942,6 +3942,10 @@
      * may provide one. Or, a carrier may decide to provide the phone number via source
      * {@link #PHONE_NUMBER_SOURCE_CARRIER carrier} if neither source UICC nor IMS is available.
      *
+     * <p>The availability and correctness of the phone number depends on the underlying source
+     * and the network etc. Additional verification is needed to use this number for
+     * security-related or other sensitive scenarios.
+     *
      * @param subscriptionId the subscription ID, or {@link #DEFAULT_SUBSCRIPTION_ID}
      *                       for the default one.
      * @param source the source of the phone number, one of the PHONE_NUMBER_SOURCE_* constants.
@@ -4175,18 +4179,18 @@
      */
     @SystemApi
     @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION)
-    public void setUserHandle(int subscriptionId, @Nullable UserHandle userHandle) {
+    public void setSubscriptionUserHandle(int subscriptionId, @Nullable UserHandle userHandle) {
         if (!isValidSubscriptionId(subscriptionId)) {
-            throw new IllegalArgumentException("[setUserHandle]: Invalid subscriptionId: "
-                    + subscriptionId);
+            throw new IllegalArgumentException("[setSubscriptionUserHandle]: "
+                    + "Invalid subscriptionId: " + subscriptionId);
         }
 
         try {
             ISub iSub = TelephonyManager.getSubscriptionService();
             if (iSub != null) {
-                iSub.setUserHandle(userHandle, subscriptionId, mContext.getOpPackageName());
+                iSub.setSubscriptionUserHandle(userHandle, subscriptionId);
             } else {
-                throw new IllegalStateException("[setUserHandle]: "
+                throw new IllegalStateException("[setSubscriptionUserHandle]: "
                         + "subscription service unavailable");
             }
         } catch (RemoteException ex) {
@@ -4211,18 +4215,18 @@
      */
     @SystemApi
     @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION)
-    public @Nullable UserHandle getUserHandle(int subscriptionId) {
+    public @Nullable UserHandle getSubscriptionUserHandle(int subscriptionId) {
         if (!isValidSubscriptionId(subscriptionId)) {
-            throw new IllegalArgumentException("[getUserHandle]: Invalid subscriptionId: "
-                    + subscriptionId);
+            throw new IllegalArgumentException("[getSubscriptionUserHandle]: "
+                    + "Invalid subscriptionId: " + subscriptionId);
         }
 
         try {
             ISub iSub = TelephonyManager.getSubscriptionService();
             if (iSub != null) {
-                return iSub.getUserHandle(subscriptionId, mContext.getOpPackageName());
+                return iSub.getSubscriptionUserHandle(subscriptionId);
             } else {
-                throw new IllegalStateException("[getUserHandle]: "
+                throw new IllegalStateException("[getSubscriptionUserHandle]: "
                         + "subscription service unavailable");
             }
         } catch (RemoteException ex) {
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 9ecebf1..ff1b1c0 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -3323,15 +3323,14 @@
             case NETWORK_TYPE_TD_SCDMA:
                 return NETWORK_TYPE_BITMASK_TD_SCDMA;
             case NETWORK_TYPE_LTE:
-                return NETWORK_TYPE_BITMASK_LTE;
             case NETWORK_TYPE_LTE_CA:
-                return NETWORK_TYPE_BITMASK_LTE_CA;
+                return NETWORK_TYPE_BITMASK_LTE;
             case NETWORK_TYPE_NR:
                 return NETWORK_TYPE_BITMASK_NR;
             case NETWORK_TYPE_IWLAN:
                 return NETWORK_TYPE_BITMASK_IWLAN;
             case NETWORK_TYPE_IDEN:
-                return (1 << (NETWORK_TYPE_IDEN - 1));
+                return NETWORK_TYPE_BITMASK_IDEN;
             default:
                 return NETWORK_TYPE_BITMASK_UNKNOWN;
         }
@@ -13911,7 +13910,8 @@
                     NETWORK_TYPE_BITMASK_LTE,
                     NETWORK_TYPE_BITMASK_LTE_CA,
                     NETWORK_TYPE_BITMASK_NR,
-                    NETWORK_TYPE_BITMASK_IWLAN
+                    NETWORK_TYPE_BITMASK_IWLAN,
+                    NETWORK_TYPE_BITMASK_IDEN
             })
     public @interface NetworkTypeBitMask {}
 
@@ -13971,6 +13971,10 @@
      */
     public static final long NETWORK_TYPE_BITMASK_HSPA = (1 << (NETWORK_TYPE_HSPA -1));
     /**
+     * network type bitmask indicating the support of radio tech iDen.
+     */
+    public static final long NETWORK_TYPE_BITMASK_IDEN = (1 << (NETWORK_TYPE_IDEN - 1));
+    /**
      * network type bitmask indicating the support of radio tech HSPAP.
      */
     public static final long NETWORK_TYPE_BITMASK_HSPAP = (1 << (NETWORK_TYPE_HSPAP -1));
@@ -13988,12 +13992,13 @@
      */
     public static final long NETWORK_TYPE_BITMASK_LTE = (1 << (NETWORK_TYPE_LTE -1));
     /**
-     * NOT USED; this bitmask is exposed accidentally, will be deprecated in U.
+     * NOT USED; this bitmask is exposed accidentally.
      * If used, will be converted to {@link #NETWORK_TYPE_BITMASK_LTE}.
      * network type bitmask indicating the support of radio tech LTE CA (carrier aggregation).
      *
-     * @see #NETWORK_TYPE_BITMASK_LTE
+     * @deprecated Please use {@link #NETWORK_TYPE_BITMASK_LTE} instead.
      */
+    @Deprecated
     public static final long NETWORK_TYPE_BITMASK_LTE_CA = (1 << (NETWORK_TYPE_LTE_CA -1));
 
     /**
diff --git a/telephony/java/android/telephony/ims/ImsService.java b/telephony/java/android/telephony/ims/ImsService.java
index f0401534..e8642fe 100644
--- a/telephony/java/android/telephony/ims/ImsService.java
+++ b/telephony/java/android/telephony/ims/ImsService.java
@@ -204,6 +204,7 @@
 
     private IImsServiceControllerListener mListener;
     private final Object mListenerLock = new Object();
+    private final Object mExecutorLock = new Object();
     private Executor mExecutor;
 
     /**
@@ -214,10 +215,6 @@
      * vendor use Runnable::run.
      */
     public ImsService() {
-        mExecutor = ImsService.this.getExecutor();
-        if (mExecutor == null) {
-            mExecutor = Runnable::run;
-        }
     }
 
     /**
@@ -356,7 +353,7 @@
                 ImsConfigImplBase c =
                         ImsService.this.getConfigForSubscription(slotId, subId);
                 if (c != null) {
-                    c.setDefaultExecutor(mExecutor);
+                    c.setDefaultExecutor(getCachedExecutor());
                     return c.getIImsConfig();
                 } else {
                     return null;
@@ -370,7 +367,7 @@
                 ImsRegistrationImplBase r =
                         ImsService.this.getRegistrationForSubscription(slotId, subId);
                 if (r != null) {
-                    r.setDefaultExecutor(mExecutor);
+                    r.setDefaultExecutor(getCachedExecutor());
                     return r.getBinder();
                 } else {
                     return null;
@@ -383,7 +380,7 @@
             return executeMethodAsyncForResult(() -> {
                 SipTransportImplBase s =  ImsService.this.getSipTransport(slotId);
                 if (s != null) {
-                    s.setDefaultExecutor(mExecutor);
+                    s.setDefaultExecutor(getCachedExecutor());
                     return s.getBinder();
                 } else {
                     return null;
@@ -427,11 +424,21 @@
         return null;
     }
 
+    private Executor getCachedExecutor() {
+        synchronized (mExecutorLock) {
+            if (mExecutor == null) {
+                Executor e = ImsService.this.getExecutor();
+                mExecutor = (e != null) ? e : Runnable::run;
+            }
+            return mExecutor;
+        }
+    }
+
     private IImsMmTelFeature createMmTelFeatureInternal(int slotId, int subscriptionId) {
         MmTelFeature f = createMmTelFeatureForSubscription(slotId, subscriptionId);
         if (f != null) {
             setupFeature(f, slotId, ImsFeature.FEATURE_MMTEL);
-            f.setDefaultExecutor(mExecutor);
+            f.setDefaultExecutor(getCachedExecutor());
             return f.getBinder();
         } else {
             Log.e(LOG_TAG, "createMmTelFeatureInternal: null feature returned.");
@@ -443,7 +450,7 @@
         MmTelFeature f = createEmergencyOnlyMmTelFeature(slotId);
         if (f != null) {
             setupFeature(f, slotId, ImsFeature.FEATURE_MMTEL);
-            f.setDefaultExecutor(mExecutor);
+            f.setDefaultExecutor(getCachedExecutor());
             return f.getBinder();
         } else {
             Log.e(LOG_TAG, "createEmergencyOnlyMmTelFeatureInternal: null feature returned.");
@@ -454,7 +461,7 @@
     private IImsRcsFeature createRcsFeatureInternal(int slotId, int subI) {
         RcsFeature f = createRcsFeatureForSubscription(slotId, subI);
         if (f != null) {
-            f.setDefaultExecutor(mExecutor);
+            f.setDefaultExecutor(getCachedExecutor());
             setupFeature(f, slotId, ImsFeature.FEATURE_RCS);
             return f.getBinder();
         } else {
@@ -609,7 +616,8 @@
     private void executeMethodAsync(Runnable r, String errorLogName) {
         try {
             CompletableFuture.runAsync(
-                    () -> TelephonyUtils.runWithCleanCallingIdentity(r), mExecutor).join();
+                    () -> TelephonyUtils.runWithCleanCallingIdentity(r),
+                    getCachedExecutor()).join();
         } catch (CancellationException | CompletionException e) {
             Log.w(LOG_TAG, "ImsService Binder - " + errorLogName + " exception: "
                     + e.getMessage());
@@ -618,7 +626,7 @@
 
     private <T> T executeMethodAsyncForResult(Supplier<T> r, String errorLogName) {
         CompletableFuture<T> future = CompletableFuture.supplyAsync(
-                () -> TelephonyUtils.runWithCleanCallingIdentity(r), mExecutor);
+                () -> TelephonyUtils.runWithCleanCallingIdentity(r), getCachedExecutor());
         try {
             return future.get();
         } catch (ExecutionException | InterruptedException e) {
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index 0211a7f..4752cca 100755
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -323,22 +323,20 @@
       *
       * @param userHandle the user handle for this subscription
       * @param subId the unique SubscriptionInfo index in database
-      * @param callingPackage The package making the IPC.
       *
       * @throws SecurityException if doesn't have MANAGE_SUBSCRIPTION_USER_ASSOCIATION
       * @throws IllegalArgumentException if subId is invalid.
       */
-    int setUserHandle(in UserHandle userHandle, int subId, String callingPackage);
+    int setSubscriptionUserHandle(in UserHandle userHandle, int subId);
 
     /**
      * Get UserHandle for this subscription
      *
      * @param subId the unique SubscriptionInfo index in database
-     * @param callingPackage the package making the IPC
      * @return userHandle associated with this subscription.
      *
-     * @throws SecurityException if doesn't have SMANAGE_SUBSCRIPTION_USER_ASSOCIATION
+     * @throws SecurityException if doesn't have MANAGE_SUBSCRIPTION_USER_ASSOCIATION
      * @throws IllegalArgumentException if subId is invalid.
      */
-     UserHandle getUserHandle(int subId, String callingPackage);
+     UserHandle getSubscriptionUserHandle(int subId);
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
index 16753e6..ca5b2af 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt
@@ -24,6 +24,8 @@
 import androidx.test.uiautomator.Until
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import com.android.server.wm.traces.common.ComponentNameMatcher
+import com.android.server.wm.traces.common.Condition
+import com.android.server.wm.traces.common.DeviceStateDump
 import com.android.server.wm.traces.parser.toFlickerComponent
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 import java.util.regex.Pattern
@@ -47,9 +49,10 @@
         wmHelper: WindowManagerStateHelper,
         expectedWindowName: String,
         action: String?,
-        stringExtras: Map<String, String>
+        stringExtras: Map<String, String>,
+        waitConditions: Array<Condition<DeviceStateDump>>
     ) {
-        super.launchViaIntent(wmHelper, expectedWindowName, action, stringExtras)
+        super.launchViaIntent(wmHelper, expectedWindowName, action, stringExtras, waitConditions)
         waitIMEShown(wmHelper)
     }